// Copyright 2022 Clastix Labs // SPDX-License-Identifier: Apache-2.0 package datastore import ( "context" "fmt" "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" dserrors "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 dserrors.NewCreateUserError(err) } return nil } func (e *EtcdClient) UpdateUser(ctx context.Context, user, password string) error { 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 dserrors.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 dserrors.NewGrantPrivilegesError(err) } if _, err := e.Client.UserGrantRole(ctx, user, dbName); err != nil { return dserrors.NewGrantPrivilegesError(err) } return nil } func (e *EtcdClient) UserExists(ctx context.Context, user string) (bool, error) { if _, err := e.Client.UserGet(ctx, user); err != nil { // Convert gRPC error to comparable EtcdError using rpctypes.Error(), // then compare against the client-side error constant. // The == comparison is correct here as rpctypes.Error() normalizes // gRPC status errors to comparable EtcdError struct values. if rpctypes.Error(err) == rpctypes.ErrUserNotFound { //nolint:errorlint return false, nil } return false, dserrors.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 { // Convert gRPC error to comparable EtcdError using rpctypes.Error(), // then compare against the client-side error constant. // The == comparison is correct here as rpctypes.Error() normalizes // gRPC status errors to comparable EtcdError struct values. if rpctypes.Error(err) == rpctypes.ErrRoleNotFound { //nolint:errorlint return false, nil } return false, dserrors.NewCheckGrantExistsError(err) } user, err := e.Client.UserGet(ctx, username) if err != nil { return false, dserrors.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 dserrors.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 dserrors.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 dserrors.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 dserrors.NewCloseConnectionError(err) } return nil } func (e *EtcdClient) Check(ctx context.Context) error { if _, err := e.Client.AuthStatus(ctx); err != nil { return dserrors.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 }