diff --git a/api/v1/duros_types.go b/api/v1/duros_types.go index 2cae157..d92ab96 100644 --- a/api/v1/duros_types.go +++ b/api/v1/duros_types.go @@ -55,10 +55,6 @@ type DurosSpec struct { // DurosStatus defines the observed state of Duros type DurosStatus struct { - // SecretRef to the create JWT Token - // TODO, this can be used to detect required key rotation - SecretRef string `json:"secret,omitempty" description:"Reference to JWT Token generated on the duros storage side for this project"` - // ManagedResourceStatuses contains a list of statuses of resources managed by this controller ManagedResourceStatuses []ManagedResourceStatus `json:"managedResourceStatuses" description:"A list of managed resource statuses"` } diff --git a/config/crd/bases/storage.metal-stack.io_duros.yaml b/config/crd/bases/storage.metal-stack.io_duros.yaml index 2a8d92f..634e616 100644 --- a/config/crd/bases/storage.metal-stack.io_duros.yaml +++ b/config/crd/bases/storage.metal-stack.io_duros.yaml @@ -101,10 +101,6 @@ spec: - state type: object type: array - secret: - description: SecretRef to the create JWT Token TODO, this can be used - to detect required key rotation - type: string required: - managedResourceStatuses type: object diff --git a/controllers/duros_controller.go b/controllers/duros_controller.go index e475e96..1851d04 100644 --- a/controllers/duros_controller.go +++ b/controllers/duros_controller.go @@ -124,12 +124,11 @@ func (r *DurosReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl } log.Info("created credential", "id", cred.ID, "project", cred.ProjectName) - // Deploy StorageClass Secret - err = r.deployStorageClassSecret(ctx, cred, r.AdminKey) + err = r.reconcileStorageClassSecret(ctx, cred, r.AdminKey) if err != nil { return requeue, err } - // Deploy CSI + err = r.deployCSI(ctx, projectID, storageClasses) if err != nil { return requeue, err @@ -154,8 +153,6 @@ func (r *DurosReconciler) reconcileStatus(ctx context.Context, duros *storagev1. sts = &appsv1.StatefulSet{} ) - duros.Status.SecretRef = "" // TODO? - err := r.Shoot.Get(ctx, types.NamespacedName{Name: lbCSINodeName, Namespace: namespace}, ds) if err != nil { return fmt.Errorf("error getting daemon set: %w", err) diff --git a/controllers/resources.go b/controllers/resources.go index 2191ef6..2b92317 100644 --- a/controllers/resources.go +++ b/controllers/resources.go @@ -7,6 +7,7 @@ import ( "time" "github.com/go-logr/logr" + "github.com/golang-jwt/jwt/v4" "github.com/metal-stack/duros-go" durosv2 "github.com/metal-stack/duros-go/api/duros/v2" @@ -38,6 +39,9 @@ const ( lbCSIControllerName = "lb-csi-controller" lbCSINodeName = "lb-csi-node" + + tokenLifetime = 8 * 24 * time.Hour + tokenRenewalBefore = 1 * 24 * time.Hour ) var ( @@ -728,15 +732,68 @@ var ( } ) -func (r *DurosReconciler) deployStorageClassSecret(ctx context.Context, credential *durosv2.Credential, adminKey []byte) error { +func (r *DurosReconciler) reconcileStorageClassSecret(ctx context.Context, credential *durosv2.Credential, adminKey []byte) error { + var ( + log = r.Log.WithName("storage-class") + secret = &corev1.Secret{} + ) + + key := types.NamespacedName{Name: storageClassCredentialsRef, Namespace: namespace} + err := r.Shoot.Get(ctx, key, secret) + if err != nil && apierrors.IsNotFound(err) { + log.Info("deploy storage-class-secret") + return r.deployStorageClassSecret(ctx, log, credential, adminKey) + } + if err != nil { + return fmt.Errorf("unable to read secret: %w", err) + } + + // secret already exists, check for renewal + token, ok := secret.Data["jwt"] + if !ok { + log.Error(fmt.Errorf("no storage class token present in existing token"), "recreating storage-class-secret") + err := r.deleteResourceWithWait(ctx, log, deletionResource{ + Key: key, + Object: secret, + }) + if err != nil { + return err + } + return r.deployStorageClassSecret(ctx, log, credential, adminKey) + } + + claims := &jwt.StandardClaims{} + _, _, err = new(jwt.Parser).ParseUnverified(string(token), claims) + if err != nil { + log.Error(err, "storage class token not parsable, recreating storage-class-secret") + err := r.deleteResourceWithWait(ctx, log, deletionResource{ + Key: key, + Object: secret, + }) + if err != nil { + return err + } + return r.deployStorageClassSecret(ctx, log, credential, adminKey) + } + + expiresAt := time.Unix(claims.ExpiresAt, 0) + renewalAt := expiresAt.Add(-tokenRenewalBefore) + if time.Now().After(renewalAt) { + log.Info("storage class token is expiring soon, refreshing token", "expires-at", expiresAt.String()) + return r.deployStorageClassSecret(ctx, log, credential, adminKey) + } + + log.Info("storage class token is not expiring soon, not doing anything", "expires-at", expiresAt.String(), "renewal-at", renewalAt.String()) + + return nil +} + +func (r *DurosReconciler) deployStorageClassSecret(ctx context.Context, log logr.Logger, credential *durosv2.Credential, adminKey []byte) error { key, err := extract(adminKey) if err != nil { return err } - log := r.Log.WithName("storage-class") - log.Info("deploy storage-class-secret") - tokenLifetime := 360 * 24 * time.Hour token, err := duros.NewJWTTokenForCredential(r.Namespace, "duros-controller", credential, []string{credential.ProjectName + ":admin"}, tokenLifetime, key) if err != nil { return fmt.Errorf("unable to create jwt token:%w", err) @@ -754,9 +811,13 @@ func (r *DurosReconciler) deployStorageClassSecret(ctx context.Context, credenti return nil }) + if err != nil { + return err + } + log.Info("storageclasssecret", "name", storageClassCredentialsRef, "operation", op) - return err + return nil } func (r *DurosReconciler) deployCSI(ctx context.Context, projectID string, scs []storagev1.StorageClass) error { diff --git a/go.mod b/go.mod index 73cf8ed..f197b1e 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cloud.google.com/go v0.94.1 // indirect github.com/go-logr/logr v0.4.0 github.com/go-logr/zapr v0.4.0 + github.com/golang-jwt/jwt/v4 v4.0.0 github.com/google/gofuzz v1.2.0 // indirect github.com/metal-stack/duros-go v0.2.3 github.com/metal-stack/v v1.0.3 diff --git a/main.go b/main.go index c9ac90c..8861fd2 100644 --- a/main.go +++ b/main.go @@ -22,6 +22,7 @@ import ( "os" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "k8s.io/client-go/tools/clientcmd" "github.com/go-logr/zapr" @@ -97,6 +98,8 @@ func main() { cfg := zap.NewProductionConfig() cfg.Level = zap.NewAtomicLevelAt(level) + cfg.EncoderConfig.TimeKey = "timestamp" + cfg.EncoderConfig.EncodeTime = zapcore.RFC3339TimeEncoder l, err := cfg.Build() if err != nil {