mirror of
https://github.com/outbackdingo/kamaji.git
synced 2026-01-27 18:19:25 +00:00
198 lines
5.1 KiB
Go
198 lines
5.1 KiB
Go
// Copyright 2022 Clastix Labs
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package datastore
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
goerrors "github.com/pkg/errors"
|
|
"go.etcd.io/etcd/api/v3/authpb"
|
|
"go.etcd.io/etcd/api/v3/v3rpc/rpctypes"
|
|
etcdclient "go.etcd.io/etcd/client/v3"
|
|
|
|
kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
|
|
"github.com/clastix/kamaji/internal/datastore/errors"
|
|
)
|
|
|
|
func NewETCDConnection(config ConnectionConfig) (Connection, error) {
|
|
endpoints := make([]string, 0, len(config.Endpoints))
|
|
|
|
for _, ep := range config.Endpoints {
|
|
endpoints = append(endpoints, ep.String())
|
|
}
|
|
|
|
cfg := etcdclient.Config{
|
|
Endpoints: endpoints,
|
|
TLS: config.TLSConfig,
|
|
}
|
|
|
|
client, err := etcdclient.New(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &EtcdClient{
|
|
Client: *client,
|
|
}, nil
|
|
}
|
|
|
|
type EtcdClient struct {
|
|
Client etcdclient.Client
|
|
}
|
|
|
|
func (e *EtcdClient) CreateUser(ctx context.Context, user, password string) error {
|
|
if _, err := e.Client.Auth.UserAddWithOptions(ctx, user, password, &etcdclient.UserAddOptions{NoPassword: true}); err != nil {
|
|
return errors.NewCreateUserError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) CreateDB(context.Context, string) error {
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) GrantPrivileges(ctx context.Context, user, dbName string) error {
|
|
if _, err := e.Client.Auth.RoleAdd(ctx, dbName); err != nil {
|
|
return errors.NewGrantPrivilegesError(err)
|
|
}
|
|
|
|
permission := etcdclient.PermissionType(authpb.READWRITE)
|
|
key := e.buildKey(dbName)
|
|
|
|
if _, err := e.Client.RoleGrantPermission(ctx, dbName, key, etcdclient.GetPrefixRangeEnd(key), permission); err != nil {
|
|
return errors.NewGrantPrivilegesError(err)
|
|
}
|
|
|
|
if _, err := e.Client.UserGrantRole(ctx, user, dbName); err != nil {
|
|
return errors.NewGrantPrivilegesError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) UserExists(ctx context.Context, user string) (bool, error) {
|
|
if _, err := e.Client.UserGet(ctx, user); err != nil {
|
|
if goerrors.As(err, &rpctypes.ErrGRPCUserNotFound) {
|
|
return false, nil
|
|
}
|
|
|
|
return false, errors.NewCheckUserExistsError(err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func (e *EtcdClient) DBExists(context.Context, string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (e *EtcdClient) GrantPrivilegesExists(ctx context.Context, username, dbName string) (bool, error) {
|
|
_, err := e.Client.RoleGet(ctx, dbName)
|
|
if err != nil {
|
|
if goerrors.As(err, &rpctypes.ErrGRPCRoleNotFound) {
|
|
return false, nil
|
|
}
|
|
|
|
return false, errors.NewCheckGrantExistsError(err)
|
|
}
|
|
|
|
user, err := e.Client.UserGet(ctx, username)
|
|
if err != nil {
|
|
return false, errors.NewCheckGrantExistsError(err)
|
|
}
|
|
|
|
for _, i := range user.Roles {
|
|
if i == dbName {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func (e *EtcdClient) DeleteUser(ctx context.Context, user string) error {
|
|
if _, err := e.Client.Auth.UserDelete(ctx, user); err != nil {
|
|
return errors.NewDeleteUserError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) DeleteDB(ctx context.Context, dbName string) error {
|
|
prefix := e.buildKey(dbName)
|
|
if _, err := e.Client.Delete(ctx, prefix, etcdclient.WithPrefix()); err != nil {
|
|
return errors.NewCannotDeleteDatabaseError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) RevokePrivileges(ctx context.Context, _, dbName string) error {
|
|
if _, err := e.Client.Auth.RoleDelete(ctx, dbName); err != nil {
|
|
return errors.NewRevokePrivilegesError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) GetConnectionString() string {
|
|
// There's no need for connection string in etcd client:
|
|
// it's not used by Kine
|
|
return ""
|
|
}
|
|
|
|
func (e *EtcdClient) Close() error {
|
|
if err := e.Client.Close(); err != nil {
|
|
return errors.NewCloseConnectionError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) Check(ctx context.Context) error {
|
|
if _, err := e.Client.AuthStatus(ctx); err != nil {
|
|
return errors.NewCheckConnectionError(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *EtcdClient) Driver() string {
|
|
return string(kamajiv1alpha1.EtcdDriver)
|
|
}
|
|
|
|
// buildKey adds slashes to the beginning and end of the key. This ensures that the range
|
|
// end for etcd RBAC is calculated using the entire key prefix, not only the key name. If
|
|
// the range end was calculated e.g. for `/cp-a`, the result would be `/cp-b`, which also
|
|
// includes `/cp-aa` (etcd uses lexicographic ordering on key ranges for RBAC). Using
|
|
// `/cp-a/` as the input for the range end calculation results in `/cp-a0`, which doesn't
|
|
// allow for any other potential control plane key prefixes to be located within the range.
|
|
// For more information, see also https://etcd.io/docs/v3.3/learning/api/#key-ranges
|
|
func (e *EtcdClient) buildKey(key string) string {
|
|
return fmt.Sprintf("/%s/", key)
|
|
}
|
|
|
|
func (e *EtcdClient) Migrate(ctx context.Context, tcp kamajiv1alpha1.TenantControlPlane, target Connection) error {
|
|
targetClient := target.(*EtcdClient) //nolint:forcetypeassert
|
|
|
|
if err := target.Check(ctx); err != nil {
|
|
return err
|
|
}
|
|
|
|
response, err := e.Client.Get(ctx, e.buildKey(tcp.Status.Storage.Setup.Schema), etcdclient.WithPrefix())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, kv := range response.Kvs {
|
|
if _, err = targetClient.Client.Put(ctx, string(kv.Key), string(kv.Value)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|