Skip to content

Commit

Permalink
Merge pull request #17 from vshn/delete-users
Browse files Browse the repository at this point in the history
Add support for deleting objectsusers on cloudscale.ch
  • Loading branch information
ccremer authored Jul 13, 2022
2 parents 321cb82 + 13a55ae commit 0ed0727
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 55 deletions.
40 changes: 6 additions & 34 deletions operator/cloudscale/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@ import (

pipeline "github.com/ccremer/go-command-pipeline"
cloudscalesdk "github.com/cloudscale-ch/cloudscale-go-sdk/v2"
bucketv1 "github.com/vshn/appcat-service-s3/apis/bucket/v1"
cloudscalev1 "github.com/vshn/appcat-service-s3/apis/cloudscale/v1"
"github.com/vshn/appcat-service-s3/operator/steps"
"golang.org/x/oauth2"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// APIToken is the authentication token to use against cloudscale.ch API
Expand Down Expand Up @@ -50,7 +45,7 @@ func CreateObjectsUser(ctx context.Context) error {
user.Status.UserID = csUser.ID

pipeline.StoreInContext(ctx, CloudscaleUserKey{}, csUser)
return logIfNotError(err, log, 1, "Created objects user in cloudscale")
return logIfNotError(err, log, 1, "Created objects user in cloudscale", "userID", csUser.ID)
}

// GetObjectsUser fetches an existing objects user from the project associated with the API token.
Expand All @@ -65,37 +60,14 @@ func GetObjectsUser(ctx context.Context) error {
return logIfNotError(err, log, 1, "Fetched objects user in cloudscale")
}

// UserCredentialSecretKey identifies the credential Secret in the context.
type UserCredentialSecretKey struct{}

// EnsureCredentialSecret creates the credential secret.
func EnsureCredentialSecret(ctx context.Context) error {
kube := steps.GetClientFromContext(ctx)
// DeleteObjectsUser deletes the objects user from the project associated with the API token.
func DeleteObjectsUser(ctx context.Context) error {
csClient := steps.GetFromContextOrPanic(ctx, CloudscaleClientKey{}).(*cloudscalesdk.Client)
user := steps.GetFromContextOrPanic(ctx, ObjectsUserKey{}).(*cloudscalev1.ObjectsUser)
csUser := steps.GetFromContextOrPanic(ctx, CloudscaleUserKey{}).(*cloudscalesdk.ObjectsUser)
log := controllerruntime.LoggerFrom(ctx)

secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: user.Spec.SecretRef, Namespace: user.Namespace}}

if keyErr := checkUserForKeys(csUser); keyErr != nil {
return keyErr
}

// See https://www.cloudscale.ch/en/api/v1#objects-users

_, err := controllerruntime.CreateOrUpdate(ctx, kube, secret, func() error {
secret.Labels = labels.Merge(secret.Labels, getCommonLabels(user.Name))
if secret.StringData == nil {
secret.StringData = map[string]string{}
}
secret.StringData[bucketv1.AccessKeyIDName] = csUser.Keys[0]["access_key"]
secret.StringData[bucketv1.SecretAccessKeyName] = csUser.Keys[0]["secret_key"]
controllerutil.AddFinalizer(secret, userFinalizer)
return controllerutil.SetOwnerReference(user, secret, kube.Scheme())
})

pipeline.StoreInContext(ctx, UserCredentialSecretKey{}, secret)
return logIfNotError(err, log, 1, "Ensured credential secret", "secretName", user.Spec.SecretRef)
err := csClient.ObjectsUsers.Delete(ctx, user.Status.UserID)
return logIfNotError(err, log, 1, "Deleted objects user in cloudscale", "userID", user.Status.UserID)
}

func checkUserForKeys(user *cloudscalesdk.ObjectsUser) error {
Expand Down
6 changes: 4 additions & 2 deletions operator/cloudscale/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (r *ObjectsUserReconciler) Reconcile(ctx context.Context, request reconcile
func (r *ObjectsUserReconciler) Provision(ctx context.Context) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Provisioning resource")
p := NewObjectsUserPipeline()
p := NewProvisioningPipeline()
err := p.Run(ctx)
return reconcile.Result{}, err
}
Expand All @@ -69,7 +69,9 @@ func (r *ObjectsUserReconciler) Provision(ctx context.Context) (reconcile.Result
func (r *ObjectsUserReconciler) Delete(ctx context.Context) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Deleting resource")
return reconcile.Result{Requeue: true}, nil
p := NewDeletionPipeline()
err := p.Run(ctx)
return reconcile.Result{Requeue: true}, err
}

func logIfNotError(err error, log logr.Logger, level int, msg string, keysAndValues ...any) error {
Expand Down
67 changes: 67 additions & 0 deletions operator/cloudscale/deletion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cloudscale

import (
"context"

pipeline "github.com/ccremer/go-command-pipeline"
cloudscalev1 "github.com/vshn/appcat-service-s3/apis/cloudscale/v1"
"github.com/vshn/appcat-service-s3/apis/conditions"
"github.com/vshn/appcat-service-s3/operator/steps"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// DeletionPipeline deletes ObjectsUsers on cloudscale.ch
type DeletionPipeline struct{}

// NewDeletionPipeline returns a new instance of DeletionPipeline.
func NewDeletionPipeline() *DeletionPipeline {
return &DeletionPipeline{}
}

// Run executes the business logic.
func (p *DeletionPipeline) Run(ctx context.Context) error {
pipe := pipeline.NewPipeline().WithBeforeHooks(steps.DebugLogger(ctx)).
WithSteps(
pipeline.If(isObjectsUserIDKnown, pipeline.NewPipeline().WithNestedSteps("deprovision objects user",
pipeline.NewStepFromFunc("create client", CreateCloudscaleClientFn(APIToken)),
pipeline.NewStepFromFunc("delete objects user", DeleteObjectsUser),
// Note: We do not need to check if there are Bucket resources still requiring the Secret.
// Cloudscale's API returns an error if there are still buckets existing for that user, which ultimately also ends up as a Failed condition in the ObjectsUser resource.
pipeline.NewStepFromFunc("delete finalizer from secret", deleteFinalizerFromSecret),
pipeline.NewStepFromFunc("emit event", emitDeletionEvent),
)),
pipeline.NewStepFromFunc("remove finalizer", steps.RemoveFinalizerFn(ObjectsUserKey{}, userFinalizer)),
).
WithFinalizer(steps.ErrorHandlerFn(ObjectsUserKey{}, conditions.ReasonDeletionFailed))
result := pipe.RunWithContext(ctx)
return result.Err()
}

func deleteFinalizerFromSecret(ctx context.Context) error {
kube := steps.GetClientFromContext(ctx)
user := steps.GetFromContextOrPanic(ctx, ObjectsUserKey{}).(*cloudscalev1.ObjectsUser)
log := controllerruntime.LoggerFrom(ctx)

secret := &corev1.Secret{}
err := kube.Get(ctx, types.NamespacedName{Name: user.Spec.SecretRef, Namespace: user.Namespace}, secret)
if apierrors.IsNotFound(err) {
return nil // doesn't exist anymore, ignore
}
if err != nil {
return err // some other error
}
err = steps.RemoveFinalizerFn(ObjectsUserKey{}, userFinalizer)(ctx)
return logIfNotError(err, log, 1, "Deleted finalizer from credentials secret", "secretName", user.Spec.SecretRef)
}

func emitDeletionEvent(ctx context.Context) error {
recorder := steps.GetEventRecorderFromContext(ctx)
user := steps.GetFromContextOrPanic(ctx, ObjectsUserKey{}).(client.Object)

recorder.Event(user, corev1.EventTypeNormal, "Deleted", "ObjectsUser deleted")
return nil
}
60 changes: 48 additions & 12 deletions operator/cloudscale/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ import (
"strings"

pipeline "github.com/ccremer/go-command-pipeline"
cloudscalesdk "github.com/cloudscale-ch/cloudscale-go-sdk/v2"
bucketv1 "github.com/vshn/appcat-service-s3/apis/bucket/v1"
cloudscalev1 "github.com/vshn/appcat-service-s3/apis/cloudscale/v1"
"github.com/vshn/appcat-service-s3/apis/conditions"
"github.com/vshn/appcat-service-s3/operator/steps"
v1 "k8s.io/api/core/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// ObjectsUserPipeline provisions ObjectsUsers on cloudscale.ch
type ObjectsUserPipeline struct {
}
// ProvisioningPipeline provisions ObjectsUsers on cloudscale.ch
type ProvisioningPipeline struct{}

// NewObjectsUserPipeline returns a new instance of ObjectsUserPipeline.
func NewObjectsUserPipeline() *ObjectsUserPipeline {
return &ObjectsUserPipeline{}
// NewProvisioningPipeline returns a new instance of ProvisioningPipeline.
func NewProvisioningPipeline() *ProvisioningPipeline {
return &ProvisioningPipeline{}
}

// Run executes the business logic.
func (p *ObjectsUserPipeline) Run(ctx context.Context) error {
func (p *ProvisioningPipeline) Run(ctx context.Context) error {
pipe := pipeline.NewPipeline().WithBeforeHooks(steps.DebugLogger(ctx)).
WithSteps(
pipeline.NewStepFromFunc("add finalizer", steps.AddFinalizerFn(ObjectsUserKey{}, userFinalizer)),
Expand All @@ -34,10 +38,10 @@ func (p *ObjectsUserPipeline) Run(ctx context.Context) error {
pipeline.NewPipeline().WithNestedSteps("new user",
pipeline.NewStepFromFunc("create objects user", CreateObjectsUser),
pipeline.NewStepFromFunc("set user in status", steps.UpdateStatusFn(ObjectsUserKey{})),
pipeline.NewStepFromFunc("emit event", emitSuccessEvent),
pipeline.NewStepFromFunc("emit event", emitCreationEvent),
),
),
pipeline.NewStepFromFunc("ensure credential secret", EnsureCredentialSecret),
pipeline.NewStepFromFunc("ensure credential secret", ensureCredentialSecret),
pipeline.NewStepFromFunc("set status condition", steps.MarkObjectReadyFn(ObjectsUserKey{})),
).
WithFinalizer(steps.ErrorHandlerFn(ObjectsUserKey{}, conditions.ReasonProvisioningFailed))
Expand All @@ -50,14 +54,46 @@ func isObjectsUserIDKnown(ctx context.Context) bool {
return user.Status.UserID != ""
}

func emitSuccessEvent(ctx context.Context) error {
func emitCreationEvent(ctx context.Context) error {
recorder := steps.GetEventRecorderFromContext(ctx)
user := steps.GetFromContextOrPanic(ctx, ObjectsUserKey{}).(client.Object)

recorder.Event(user, v1.EventTypeNormal, "Created", "ObjectsUser successfully created")
recorder.Event(user, corev1.EventTypeNormal, "Created", "ObjectsUser successfully created")
return nil
}

// UserCredentialSecretKey identifies the credential Secret in the context.
type UserCredentialSecretKey struct{}

func ensureCredentialSecret(ctx context.Context) error {
kube := steps.GetClientFromContext(ctx)
user := steps.GetFromContextOrPanic(ctx, ObjectsUserKey{}).(*cloudscalev1.ObjectsUser)
csUser := steps.GetFromContextOrPanic(ctx, CloudscaleUserKey{}).(*cloudscalesdk.ObjectsUser)
log := controllerruntime.LoggerFrom(ctx)

secret := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: user.Spec.SecretRef, Namespace: user.Namespace}}

if keyErr := checkUserForKeys(csUser); keyErr != nil {
return keyErr
}

// See https://www.cloudscale.ch/en/api/v1#objects-users

_, err := controllerruntime.CreateOrUpdate(ctx, kube, secret, func() error {
secret.Labels = labels.Merge(secret.Labels, getCommonLabels(user.Name))
if secret.StringData == nil {
secret.StringData = map[string]string{}
}
secret.StringData[bucketv1.AccessKeyIDName] = csUser.Keys[0]["access_key"]
secret.StringData[bucketv1.SecretAccessKeyName] = csUser.Keys[0]["secret_key"]
controllerutil.AddFinalizer(secret, userFinalizer)
return controllerutil.SetOwnerReference(user, secret, kube.Scheme())
})

pipeline.StoreInContext(ctx, UserCredentialSecretKey{}, secret)
return logIfNotError(err, log, 1, "Ensured credential secret", "secretName", user.Spec.SecretRef)
}

func getCommonLabels(instanceName string) labels.Set {
// https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
return labels.Set{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package cloudscale

import (
"context"
"testing"

pipeline "github.com/ccremer/go-command-pipeline"
cloudscalesdk "github.com/cloudscale-ch/cloudscale-go-sdk/v2"
"github.com/stretchr/testify/suite"
Expand All @@ -13,23 +15,22 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"testing"
)

type CloudscaleClientSuite struct {
type ProvisionPipelineSuite struct {
operatortest.Suite
}

func TestFinalizerSuite(t *testing.T) {
suite.Run(t, new(CloudscaleClientSuite))
func TestProvisionPipelineSuite(t *testing.T) {
suite.Run(t, new(ProvisionPipelineSuite))
}

func (ts *CloudscaleClientSuite) BeforeTest(suiteName, testName string) {
func (ts *ProvisionPipelineSuite) BeforeTest(suiteName, testName string) {
ts.Context = pipeline.MutableContext(context.Background())
steps.SetClientInContext(ts.Context, ts.Client)
}

func (ts *CloudscaleClientSuite) Test_EnsureCredentialSecretFn() {
func (ts *ProvisionPipelineSuite) Test_EnsureCredentialSecretFn() {
// Arrange
user := &cloudscalev1.ObjectsUser{
ObjectMeta: metav1.ObjectMeta{Name: "user", Namespace: "namespace", UID: "uid"},
Expand All @@ -44,7 +45,7 @@ func (ts *CloudscaleClientSuite) Test_EnsureCredentialSecretFn() {
ts.EnsureNS(user.Namespace)

// Act
err := EnsureCredentialSecret(ts.Context)
err := ensureCredentialSecret(ts.Context)
ts.Require().NoError(err)

// Assert
Expand Down

0 comments on commit 0ed0727

Please sign in to comment.