diff --git a/internal/fixture/fixture.go b/internal/fixture/fixture.go index 859da5847a..d93b2f98ea 100644 --- a/internal/fixture/fixture.go +++ b/internal/fixture/fixture.go @@ -343,6 +343,27 @@ func FixRuntimeState(id, runtimeID, operationID string) internal.RuntimeState { } } +func FixBinding(id string) internal.Binding { + var instanceID = fmt.Sprintf("instance-%s", id) + + return FixBindingWithInstanceID(id, instanceID) +} + +func FixBindingWithInstanceID(bindingID string, instanceID string) internal.Binding { + return internal.Binding{ + ID: bindingID, + InstanceID: instanceID, + + CreatedAt: time.Now(), + UpdatedAt: time.Now().Add(time.Minute * 5), + + Kubeconfig: "kubeconfig", + ExpirationSeconds: 600, + GenerationMethod: "adminkubeconfig", + BindingType: internal.BINDING_TYPE_SERVICE_ACCOUNT, + } +} + // SimpleInputCreator implements ProvisionerInputCreator interface func (c *SimpleInputCreator) SetProvisioningParameters(params internal.ProvisioningParameters) internal.ProvisionerInputCreator { return c diff --git a/internal/model.go b/internal/model.go index 4f496cc63d..ee2a199449 100644 --- a/internal/model.go +++ b/internal/model.go @@ -17,6 +17,9 @@ import ( log "github.com/sirupsen/logrus" ) +const BINDING_TYPE_SERVICE_ACCOUNT = "service_account" +const BINDING_TYPE_ADMIN_KUBECONFIG = "gardener_admin_kubeconfig" + type ProvisionerInputCreator interface { SetProvisioningParameters(params ProvisioningParameters) ProvisionerInputCreator SetShootName(string) ProvisionerInputCreator @@ -579,3 +582,16 @@ type DeletedStats struct { NumberOfDeletedInstances int NumberOfOperationsForDeletedInstances int } + +type Binding struct { + ID string + InstanceID string + + CreatedAt time.Time + UpdatedAt time.Time + + Kubeconfig string + ExpirationSeconds int64 + GenerationMethod string + BindingType string +} diff --git a/internal/storage/dbmodel/binding.go b/internal/storage/dbmodel/binding.go new file mode 100644 index 0000000000..1a90b7af99 --- /dev/null +++ b/internal/storage/dbmodel/binding.go @@ -0,0 +1,17 @@ +package dbmodel + +import ( + "time" +) + +type BindingDTO struct { + ID string + InstanceID string + + CreatedAt time.Time + + Kubeconfig string + ExpirationSeconds int64 + GenerationMethod string + BindingType string +} diff --git a/internal/storage/driver/postsql/binding.go b/internal/storage/driver/postsql/binding.go new file mode 100644 index 0000000000..d94ec88d29 --- /dev/null +++ b/internal/storage/driver/postsql/binding.go @@ -0,0 +1,114 @@ +package postsql + +import ( + "fmt" + + "github.com/kyma-project/kyma-environment-broker/internal" + "github.com/kyma-project/kyma-environment-broker/internal/storage/dberr" + "github.com/kyma-project/kyma-environment-broker/internal/storage/dbmodel" + "github.com/kyma-project/kyma-environment-broker/internal/storage/postsql" + log "github.com/sirupsen/logrus" +) + +type Binding struct { + postsql.Factory + cipher Cipher +} + +func NewBinding(sess postsql.Factory, cipher Cipher) *Binding { + return &Binding{ + Factory: sess, + cipher: cipher, + } +} + +func (s *Binding) GetByBindingID(bindingId string) (*internal.Binding, error) { + sess := s.NewReadSession() + bindingDTO := dbmodel.BindingDTO{} + bindingDTO, lastErr := sess.GetBindingByID(bindingId) + if lastErr != nil { + if dberr.IsNotFound(lastErr) { + return nil, dberr.NotFound("Binding with id %s not exist", bindingId) + } + log.Errorf("while getting instanceDTO by ID %s: %v", bindingId, lastErr) + return nil, lastErr + } + binding, err := s.toBinding(bindingDTO) + if err != nil { + return nil, err + } + + return &binding, nil +} + +func (s *Binding) Insert(binding *internal.Binding) error { + _, err := s.GetByBindingID(binding.ID) + if err == nil { + return dberr.AlreadyExists("instance with id %s already exist", binding.ID) + } + + dto, err := s.toBindingDTO(binding) + if err != nil { + return err + } + + sess := s.NewWriteSession() + err = sess.InsertBinding(dto) + if err != nil { + return fmt.Errorf("while saving binding with ID %s: %w", binding.ID, err) + } + + return nil +} + +func (s *Binding) DeleteByBindingID(ID string) error { + sess := s.NewWriteSession() + return sess.DeleteBinding(ID) +} + +func (s *Binding) ListByInstanceID(instanceID string) ([]internal.Binding, error) { + dtos, err := s.NewReadSession().ListBindings(instanceID) + if err != nil { + return []internal.Binding{}, err + } + var bindings []internal.Binding + for _, dto := range dtos { + instance, err := s.toBinding(dto) + if err != nil { + return []internal.Binding{}, err + } + + bindings = append(bindings, instance) + } + return bindings, err +} + +func (s *Binding) toBindingDTO(binding *internal.Binding) (dbmodel.BindingDTO, error) { + encrypted, err := s.cipher.Encrypt([]byte(binding.Kubeconfig)) + if err != nil { + return dbmodel.BindingDTO{}, fmt.Errorf("while encrypting kubeconfig: %w", err) + } + + return dbmodel.BindingDTO{ + Kubeconfig: string(encrypted), + ID: binding.ID, + InstanceID: binding.InstanceID, + CreatedAt: binding.CreatedAt, + ExpirationSeconds: binding.ExpirationSeconds, + }, nil +} + +func (s *Binding) toBinding(dto dbmodel.BindingDTO) (internal.Binding, error) { + decrypted, err := s.cipher.Decrypt([]byte(dto.Kubeconfig)) + if err != nil { + return internal.Binding{}, fmt.Errorf("while decrypting kubeconfig: %w", err) + } + + return internal.Binding{ + Kubeconfig: string(decrypted), + ID: dto.ID, + InstanceID: dto.InstanceID, + CreatedAt: dto.CreatedAt, + ExpirationSeconds: dto.ExpirationSeconds, + }, nil +} diff --git a/internal/storage/driver/postsql/binding_test.go b/internal/storage/driver/postsql/binding_test.go new file mode 100644 index 0000000000..8d20f3a763 --- /dev/null +++ b/internal/storage/driver/postsql/binding_test.go @@ -0,0 +1,222 @@ +package postsql_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/kyma-project/kyma-environment-broker/internal/fixture" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBinding(t *testing.T) { + + t.Run("should create, load and delete binding without errors", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + testBindingId := "test" + fixedBinding := fixture.FixBinding(testBindingId) + + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + // when + createdBinding, err := brokerStorage.Bindings().GetByBindingID(testBindingId) + + // then + assert.NoError(t, err) + assert.NotNil(t, createdBinding.ID) + assert.Equal(t, fixedBinding.ID, createdBinding.ID) + assert.NotNil(t, createdBinding.InstanceID) + assert.Equal(t, fixedBinding.InstanceID, createdBinding.InstanceID) + assert.NotNil(t, createdBinding.ExpirationSeconds) + assert.Equal(t, fixedBinding.ExpirationSeconds, createdBinding.ExpirationSeconds) + assert.NotNil(t, createdBinding.Kubeconfig) + assert.Equal(t, fixedBinding.Kubeconfig, createdBinding.Kubeconfig) + + // when + err = brokerStorage.Bindings().DeleteByBindingID(testBindingId) + + // then + nonExisting, err := brokerStorage.Bindings().GetByBindingID(testBindingId) + assert.Error(t, err) + assert.Nil(t, nonExisting) + }) + + t.Run("should return error when the same object inserted twice", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + testBindingId := "test" + fixedBinding := fixture.FixBinding(testBindingId) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + // then + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.Error(t, err) + }) + + t.Run("should succeed when the same object is deleted twice", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + testBindingId := "test" + fixedBinding := fixture.FixBinding(testBindingId) + + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + err = brokerStorage.Bindings().DeleteByBindingID(fixedBinding.ID) + assert.NoError(t, err) + + // then + err = brokerStorage.Bindings().DeleteByBindingID(fixedBinding.ID) + assert.NoError(t, err) + }) + + t.Run("should list all created bindings", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + sameInstanceID := uuid.New().String() + fixedBinding := fixture.FixBindingWithInstanceID("1", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + fixedBinding = fixture.FixBindingWithInstanceID("2", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + fixedBinding = fixture.FixBindingWithInstanceID("3", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + // when + bindings, err := brokerStorage.Bindings().ListByInstanceID(sameInstanceID) + + // then + assert.NoError(t, err) + assert.Len(t, bindings, 3) + }) + + t.Run("should return bindings only for given instance", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + sameInstanceID := uuid.New().String() + differentInstanceID := uuid.New().String() + fixedBinding := fixture.FixBindingWithInstanceID("1", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + fixedBinding = fixture.FixBindingWithInstanceID("2", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + fixedBinding = fixture.FixBindingWithInstanceID("3", differentInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + // when + bindings, err := brokerStorage.Bindings().ListByInstanceID(sameInstanceID) + + // then + assert.NoError(t, err) + assert.Len(t, bindings, 2) + + for _, binding := range bindings { + assert.Equal(t, sameInstanceID, binding.InstanceID) + } + }) + + t.Run("should return empty list if no bindings exist for given instance", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + sameInstanceID := uuid.New().String() + fixedBinding := fixture.FixBindingWithInstanceID("1", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + fixedBinding = fixture.FixBindingWithInstanceID("2", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + fixedBinding = fixture.FixBindingWithInstanceID("3", sameInstanceID) + err = brokerStorage.Bindings().Insert(&fixedBinding) + assert.NoError(t, err) + + // when + bindings, err := brokerStorage.Bindings().ListByInstanceID(uuid.New().String()) + + // then + assert.NoError(t, err) + assert.Len(t, bindings, 0) + + for _, binding := range bindings { + assert.Equal(t, sameInstanceID, binding.InstanceID) + } + }) + + t.Run("should return empty list if no bindings exist for given instance", func(t *testing.T) { + storageCleanup, brokerStorage, err := GetStorageForDatabaseTests() + require.NoError(t, err) + require.NotNil(t, brokerStorage) + defer func() { + err := storageCleanup() + assert.NoError(t, err) + }() + + // given + sameInstanceID := uuid.New().String() + + // when + bindings, err := brokerStorage.Bindings().ListByInstanceID(sameInstanceID) + + // then + assert.NoError(t, err) + assert.Len(t, bindings, 0) + + for _, binding := range bindings { + assert.Equal(t, sameInstanceID, binding.InstanceID) + } + }) +} diff --git a/internal/storage/ext.go b/internal/storage/ext.go index 4475862b40..bb03b4fad8 100644 --- a/internal/storage/ext.go +++ b/internal/storage/ext.go @@ -127,3 +127,10 @@ type SubaccountStates interface { DeleteState(subaccountID string) error ListStates() ([]internal.SubaccountState, error) } + +type Bindings interface { + Insert(binding *internal.Binding) error + GetByBindingID(bindingID string) (*internal.Binding, error) + ListByInstanceID(instanceID string) ([]internal.Binding, error) + DeleteByBindingID(bindingID string) error +} diff --git a/internal/storage/postsql/factory.go b/internal/storage/postsql/factory.go index dc8d2503e6..e413a2bcea 100644 --- a/internal/storage/postsql/factory.go +++ b/internal/storage/postsql/factory.go @@ -61,6 +61,8 @@ type ReadSession interface { TotalNumberOfInstancesArchivedForGlobalAccountID(globalAccountID string, planID string) (int, error) GetAllOperations() ([]dbmodel.OperationDTO, error) ListInstancesArchived(filter dbmodel.InstanceFilter) ([]dbmodel.InstanceArchivedDTO, int, int, error) + GetBindingByID(instanceID string) (dbmodel.BindingDTO, dberr.Error) + ListBindings(instanceID string) ([]dbmodel.BindingDTO, error) } //go:generate mockery --name=WriteSession @@ -80,6 +82,8 @@ type WriteSession interface { DeleteRuntimeStatesByOperationID(operationID string) error DeleteOperationByID(operationID string) dberr.Error InsertInstanceArchived(instance dbmodel.InstanceArchivedDTO) dberr.Error + InsertBinding(binding dbmodel.BindingDTO) dberr.Error + DeleteBinding(ID string) dberr.Error } type Transaction interface { diff --git a/internal/storage/postsql/init.go b/internal/storage/postsql/init.go index 83892182c8..d3b687becc 100644 --- a/internal/storage/postsql/init.go +++ b/internal/storage/postsql/init.go @@ -20,6 +20,7 @@ const ( SubaccountStatesTableName = "subaccount_states" CreatedAtField = "created_at" InstancesArchivedTableName = "instances_archived" + BindingsTableName = "bindings" ) // InitializeDatabase opens database connection and initializes schema if it does not exist diff --git a/internal/storage/postsql/read.go b/internal/storage/postsql/read.go index b3016f4def..2bef6d502a 100644 --- a/internal/storage/postsql/read.go +++ b/internal/storage/postsql/read.go @@ -21,6 +21,39 @@ type readSession struct { session *dbr.Session } +func (r readSession) GetBindingByID(bindingID string) (dbmodel.BindingDTO, dberr.Error) { + var binding dbmodel.BindingDTO + + err := r.session. + Select("*"). + From(BindingsTableName). + Where(dbr.Eq("id", bindingID)). + LoadOne(&binding) + + if err != nil { + if err == dbr.ErrNotFound { + return dbmodel.BindingDTO{}, dberr.NotFound("Cannot find Binding for bindingId:'%s'", bindingID) + } + return dbmodel.BindingDTO{}, dberr.Internal("Failed to get Instance: %s", err) + } + + return binding, nil +} + +func (r readSession) ListBindings(instanceID string) ([]dbmodel.BindingDTO, error) { + var bindings []dbmodel.BindingDTO + if len(instanceID) == 0 { + return bindings, fmt.Errorf("instanceID cannot be empty") + } + stmt := r.session.Select("*").From(BindingsTableName) + if len(instanceID) != 0 { + stmt.Where(dbr.Eq("instance_id", instanceID)) + } + stmt.OrderBy("created_at") + _, err := stmt.Load(&bindings) + return bindings, err +} + func (r readSession) ListSubaccountStates() ([]dbmodel.SubaccountStateDTO, dberr.Error) { var states []dbmodel.SubaccountStateDTO diff --git a/internal/storage/postsql/write.go b/internal/storage/postsql/write.go index af0f5f5464..840e7839cf 100644 --- a/internal/storage/postsql/write.go +++ b/internal/storage/postsql/write.go @@ -21,6 +21,39 @@ type writeSession struct { transaction *dbr.Tx } +func (ws writeSession) DeleteBinding(ID string) dberr.Error { + _, err := ws.deleteFrom(BindingsTableName). + Where(dbr.Eq("id", ID)). + Exec() + + if err != nil { + return dberr.Internal("Failed to delete record from bindings table: %s", err) + } + return nil +} + +func (ws writeSession) InsertBinding(binding dbmodel.BindingDTO) dberr.Error { + _, err := ws.insertInto(BindingsTableName). + Pair("id", binding.ID). + Pair("instance_id", binding.InstanceID). + Pair("created_at", binding.CreatedAt). + Pair("kubeconfig", binding.Kubeconfig). + Pair("expiration_seconds", binding.ExpirationSeconds). + Pair("binding_type", binding.BindingType). + Exec() + + if err != nil { + if err, ok := err.(*pq.Error); ok { + if err.Code == UniqueViolationErrorCode { + return dberr.AlreadyExists("binding with id %s already exist for runtime %s", binding.ID, binding.InstanceID) + } + } + return dberr.Internal("Failed to insert record to Binding table: %s", err) + } + + return nil +} + func (ws writeSession) InsertInstanceArchived(instance dbmodel.InstanceArchivedDTO) dberr.Error { _, err := ws.insertInto(InstancesArchivedTableName). Pair("instance_id", instance.InstanceID). diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 26cdc7a59b..0c28fd9880 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -24,6 +24,7 @@ type BrokerStorage interface { SubaccountStates() SubaccountStates Events() Events InstancesArchived() InstancesArchived + Bindings() Bindings } const ( @@ -54,6 +55,7 @@ func NewFromConfig(cfg Config, evcfg events.Config, cipher postgres.Cipher, log events: events.New(evcfg, eventstorage.New(fact, log)), subaccountStates: postgres.NewSubaccountStates(fact), instancesArchived: postgres.NewInstanceArchived(fact), + bindings: postgres.NewBinding(fact, cipher), }, connection, nil } @@ -128,6 +130,7 @@ type storage struct { events Events subaccountStates SubaccountStates instancesArchived InstancesArchived + bindings Bindings } func (s storage) Instances() Instances { @@ -165,3 +168,7 @@ func (s storage) SubaccountStates() SubaccountStates { func (s storage) InstancesArchived() InstancesArchived { return s.instancesArchived } + +func (s storage) Bindings() Bindings { + return s.bindings +} diff --git a/resources/keb/migrations/202410030001_add_kyma_bindings.down.sql b/resources/keb/migrations/202410030001_add_kyma_bindings.down.sql new file mode 100644 index 0000000000..05864d29e6 --- /dev/null +++ b/resources/keb/migrations/202410030001_add_kyma_bindings.down.sql @@ -0,0 +1 @@ +DROP TABLE bindings; \ No newline at end of file diff --git a/resources/keb/migrations/202410030001_add_kyma_bindings.up.sql b/resources/keb/migrations/202410030001_add_kyma_bindings.up.sql new file mode 100644 index 0000000000..778ba32b74 --- /dev/null +++ b/resources/keb/migrations/202410030001_add_kyma_bindings.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS bindings ( + id VARCHAR(255) NOT NULL, + instance_id VARCHAR(255) NOT NULL, + created_at TIMESTAMPTZ NOT NULL, + -- represents algorithm used to generate a kubeconfig, initialy: adminkubeconfig or tokenrequest + binding_type VARCHAR(64) NOT NULL, + -- content of the kubeconfig + kubeconfig TEXT, + -- expiration seconds + expiration_seconds INTEGER, + -- allow for the same binding id to be used for different runtimes + PRIMARY KEY(id, instance_id) +); diff --git a/resources/keb/migrations/202410030002_add_binding_indexes.down.sql b/resources/keb/migrations/202410030002_add_binding_indexes.down.sql new file mode 100644 index 0000000000..6c6b965d8b --- /dev/null +++ b/resources/keb/migrations/202410030002_add_binding_indexes.down.sql @@ -0,0 +1 @@ +DROP INDEX bindings_by_instance_id; diff --git a/resources/keb/migrations/202410030002_add_binding_indexes.up.sql b/resources/keb/migrations/202410030002_add_binding_indexes.up.sql new file mode 100644 index 0000000000..8da3cb946f --- /dev/null +++ b/resources/keb/migrations/202410030002_add_binding_indexes.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX bindings_by_instance_id ON bindings USING btree (instance_id); +CREATE INDEX bindings_by_id ON bindings USING btree (id);