diff --git a/PROJECT b/PROJECT index 40f200690..e0bb323d2 100644 --- a/PROJECT +++ b/PROJECT @@ -164,6 +164,7 @@ resources: version: v1alpha2 webhooks: validation: true + defaulting: true webhookVersion: v1 - api: crdVersion: v1 diff --git a/api/v1alpha2/linodeobjectstoragekey_types.go b/api/v1alpha2/linodeobjectstoragekey_types.go index f02ef691a..15a8f22a0 100644 --- a/api/v1alpha2/linodeobjectstoragekey_types.go +++ b/api/v1alpha2/linodeobjectstoragekey_types.go @@ -34,6 +34,26 @@ type BucketAccessRef struct { Region string `json:"region"` } +type GeneratedSecret struct { + // The name of the generated Secret. If not set, the name is formatted as "{name-of-obj-key}-obj-key". + // +optional + Name string `json:"name,omitempty"` + // The namespace for the generated Secret. If not set, defaults to the namespace of the LinodeObjectStorageKey. + // +optional + Namespace string `json:"namespace,omitempty"` + // The type of the generated Secret. + // +kubebuilder:validation:Enum=Opaque;addons.cluster.x-k8s.io/resource-set + // +kubebuilder:default=Opaque + // +optional + Type corev1.SecretType `json:"type,omitempty"` + // How to format the data stored in the generated Secret. + // It supports Go template syntax and interpolating the following values: .AccessKey, .SecretKey. + // If no format is supplied then a generic one is used containing the values specified. + // When SecretType is set to addons.cluster.x-k8s.io/resource-set, a .BucketEndpoint value is also available pointing to the location of the first bucket specified in BucketAccess. + // +optional + Format map[string]string `json:"format,omitempty"` +} + // LinodeObjectStorageKeySpec defines the desired state of LinodeObjectStorageKey type LinodeObjectStorageKeySpec struct { // BucketAccess is the list of object storage bucket labels which can be accessed using the key @@ -43,24 +63,26 @@ type LinodeObjectStorageKeySpec struct { // CredentialsRef is a reference to a Secret that contains the credentials to use for generating access keys. // If not supplied then the credentials of the controller will be used. // +optional - CredentialsRef *corev1.SecretReference `json:"credentialsRef"` + CredentialsRef *corev1.SecretReference `json:"credentialsRef,omitempty"` // KeyGeneration may be modified to trigger a rotation of the access key. // +kubebuilder:default=0 KeyGeneration int `json:"keyGeneration"` + // GeneratedSecret configures the Secret to generate containing access key details. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + GeneratedSecret `json:"generatedSecret"` + // SecretType instructs the controller what type of secret to generate containing access key details. + // Deprecated: Use generatedSecret.type. // +kubebuilder:validation:Enum=Opaque;addons.cluster.x-k8s.io/resource-set - // +kubebuilder:default=Opaque - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +kubebuilder:deprecatedversion:warning="secretType deprecated by generatedSecret.type" // +optional SecretType corev1.SecretType `json:"secretType,omitempty"` // SecretDataFormat instructs the controller how to format the data stored in the secret containing access key details. - // It supports Go template syntax and interpolating the following values: .AccessKey, .SecretKey. - // If no format is supplied then a generic one is used containing the values specified. - // When SecretType is set to addons.cluster.x-k8s.io/resource-set, a .BucketEndpoint value is also available pointing to the location of the first bucket specified in BucketAccess. - // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // Deprecated: Use generatedSecret.format. + // +kubebuilder:deprecatedversion:warning="secretDataFormat deprecated by generatedSecret.format" // +optional SecretDataFormat map[string]string `json:"secretDataFormat,omitempty"` } @@ -90,10 +112,6 @@ type LinodeObjectStorageKeyStatus struct { // +optional LastKeyGeneration *int `json:"lastKeyGeneration,omitempty"` - // SecretName specifies the name of the Secret containing access key data. - // +optional - SecretName *string `json:"secretName,omitempty"` - // AccessKeyRef stores the ID for Object Storage key provisioned. // +optional AccessKeyRef *int `json:"accessKeyRef,omitempty"` diff --git a/api/v1alpha2/linodeobjectstoragekey_webhook.go b/api/v1alpha2/linodeobjectstoragekey_webhook.go index 89539cb7b..363ea0665 100644 --- a/api/v1alpha2/linodeobjectstoragekey_webhook.go +++ b/api/v1alpha2/linodeobjectstoragekey_webhook.go @@ -40,11 +40,14 @@ func (r *LinodeObjectStorageKey) SetupWebhookWithManager(mgr ctrl.Manager) error Complete() } -// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. -//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha2-linodeobjectstoragekey,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodeobjectstoragekeys,verbs=create;update,versions=v1alpha2,name=validation.linodeobjectstoragekey.infrastructure.cluster.x-k8s.io,admissionReviewVersions=v1 +// +kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha2-linodeobjectstoragekey,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodeobjectstoragekeys,verbs=create;update,versions=v1alpha2,name=validation.linodeobjectstoragekey.infrastructure.cluster.x-k8s.io,admissionReviewVersions=v1 var _ webhook.Validator = &LinodeObjectStorageKey{} +// +kubebuilder:webhook:path=/mutate-infrastructure-cluster-x-k8s-io-v1alpha2-linodeobjectstoragekey,mutating=true,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodeobjectstoragekeys,verbs=create;update,versions=v1alpha2,name=mutation.linodeobjectstoragekey.infrastructure.cluster.x-k8s.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &LinodeObjectStorageKey{} + // ValidateCreate implements webhook.Validator so a webhook will be registered for the type func (r *LinodeObjectStorageKey) ValidateCreate() (admission.Warnings, error) { linodeobjectstoragekeylog.Info("validate create", "name", r.Name) @@ -67,10 +70,10 @@ func (r *LinodeObjectStorageKey) ValidateDelete() (admission.Warnings, error) { func (r *LinodeObjectStorageKey) validateLinodeObjectStorageKey() (admission.Warnings, error) { var errs field.ErrorList - if r.Spec.SecretType == clusteraddonsv1.ClusterResourceSetSecretType && len(r.Spec.SecretDataFormat) == 0 { + if r.Spec.GeneratedSecret.Type == clusteraddonsv1.ClusterResourceSetSecretType && len(r.Spec.GeneratedSecret.Format) == 0 { errs = append(errs, field.Invalid( - field.NewPath("spec").Child("secretDataFormat"), - r.Spec.SecretDataFormat, + field.NewPath("spec").Child("generatedSecret").Child("format"), + r.Spec.GeneratedSecret.Format, fmt.Sprintf("must not be empty with Secret type %s", clusteraddonsv1.ClusterResourceSetSecretType), )) } @@ -81,3 +84,26 @@ func (r *LinodeObjectStorageKey) validateLinodeObjectStorageKey() (admission.War return nil, nil } + +const defaultKeySecretNameTemplate = "%s-obj-key" + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *LinodeObjectStorageKey) Default() { + linodeobjectstoragekeylog.Info("default", "name", r.Name) + + // Default name and namespace derived from object metadata. + if r.Spec.GeneratedSecret.Name == "" { + r.Spec.GeneratedSecret.Name = fmt.Sprintf(defaultKeySecretNameTemplate, r.Name) + } + if r.Spec.GeneratedSecret.Namespace == "" { + r.Spec.GeneratedSecret.Namespace = r.Namespace + } + + // Support deprecated fields when specified and updated fields are empty. + if r.Spec.SecretType != "" && r.Spec.GeneratedSecret.Type == "" { + r.Spec.GeneratedSecret.Type = r.Spec.SecretType + } + if len(r.Spec.SecretDataFormat) > 0 && len(r.Spec.GeneratedSecret.Format) == 0 { + r.Spec.GeneratedSecret.Format = r.Spec.SecretDataFormat + } +} diff --git a/api/v1alpha2/linodeobjectstoragekey_webhook_test.go b/api/v1alpha2/linodeobjectstoragekey_webhook_test.go index ac6f028b1..99de33924 100644 --- a/api/v1alpha2/linodeobjectstoragekey_webhook_test.go +++ b/api/v1alpha2/linodeobjectstoragekey_webhook_test.go @@ -22,6 +22,7 @@ import ( "testing" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clusteraddonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1" ) @@ -36,24 +37,30 @@ func TestValidateLinodeObjectStorageKey(t *testing.T) { { name: "opaque", spec: LinodeObjectStorageKeySpec{ - SecretType: corev1.SecretTypeOpaque, + GeneratedSecret: GeneratedSecret{ + Type: corev1.SecretTypeOpaque, + }, }, err: nil, }, { name: "resourceset with empty secret data format", spec: LinodeObjectStorageKeySpec{ - SecretType: clusteraddonsv1.ClusterResourceSetSecretType, - SecretDataFormat: map[string]string{}, + GeneratedSecret: GeneratedSecret{ + Type: clusteraddonsv1.ClusterResourceSetSecretType, + Format: map[string]string{}, + }, }, err: errors.New("must not be empty with Secret type"), }, { name: "valid resourceset", spec: LinodeObjectStorageKeySpec{ - SecretType: clusteraddonsv1.ClusterResourceSetSecretType, - SecretDataFormat: map[string]string{ - "file.yaml": "kind: Secret", + GeneratedSecret: GeneratedSecret{ + Type: clusteraddonsv1.ClusterResourceSetSecretType, + Format: map[string]string{ + "file.yaml": "kind: Secret", + }, }, }, err: nil, @@ -84,3 +91,44 @@ func TestValidateLinodeObjectStorageKey(t *testing.T) { }) } } + +func TestLinodeObjectStorageKeyDefault(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + genSecret GeneratedSecret + expectedName string + expectedNamespace string + }{ + {"already set", GeneratedSecret{Name: "secret", Namespace: "ns"}, "secret", "ns"}, + {"no name", GeneratedSecret{Namespace: "ns"}, "key-obj-key", "ns"}, + {"no namespace", GeneratedSecret{Name: "secret"}, "secret", "keyns"}, + } + + for _, tt := range tests { + testcase := tt + + t.Run(testcase.name, func(t *testing.T) { + t.Parallel() + + key := &LinodeObjectStorageKey{ + ObjectMeta: metav1.ObjectMeta{ + Name: "key", + Namespace: "keyns", + }, + Spec: LinodeObjectStorageKeySpec{ + GeneratedSecret: testcase.genSecret, + }, + } + + key.Default() + if key.Spec.GeneratedSecret.Name != testcase.expectedName { + t.Errorf("name: expected %s but got %s", testcase.expectedName, key.Spec.GeneratedSecret.Name) + } + if key.Spec.GeneratedSecret.Namespace != testcase.expectedNamespace { + t.Errorf("name: expected %s but got %s", testcase.expectedNamespace, key.Spec.GeneratedSecret.Namespace) + } + }) + } +} diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 747433c46..3b40ac59e 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -63,6 +63,28 @@ func (in *FirewallRule) DeepCopy() *FirewallRule { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GeneratedSecret) DeepCopyInto(out *GeneratedSecret) { + *out = *in + if in.Format != nil { + in, out := &in.Format, &out.Format + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedSecret. +func (in *GeneratedSecret) DeepCopy() *GeneratedSecret { + if in == nil { + return nil + } + out := new(GeneratedSecret) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InstanceConfigInterfaceCreateOptions) DeepCopyInto(out *InstanceConfigInterfaceCreateOptions) { *out = *in @@ -968,6 +990,7 @@ func (in *LinodeObjectStorageKeySpec) DeepCopyInto(out *LinodeObjectStorageKeySp *out = new(v1.SecretReference) **out = **in } + in.GeneratedSecret.DeepCopyInto(&out.GeneratedSecret) if in.SecretDataFormat != nil { in, out := &in.SecretDataFormat, &out.SecretDataFormat *out = make(map[string]string, len(*in)) @@ -1011,11 +1034,6 @@ func (in *LinodeObjectStorageKeyStatus) DeepCopyInto(out *LinodeObjectStorageKey *out = new(int) **out = **in } - if in.SecretName != nil { - in, out := &in.SecretName, &out.SecretName - *out = new(string) - **out = **in - } if in.AccessKeyRef != nil { in, out := &in.AccessKeyRef, &out.AccessKeyRef *out = new(int) diff --git a/cloud/scope/object_storage_key.go b/cloud/scope/object_storage_key.go index d0895ca0d..7bbd248ed 100644 --- a/cloud/scope/object_storage_key.go +++ b/cloud/scope/object_storage_key.go @@ -99,8 +99,6 @@ func (s *ObjectStorageKeyScope) AddFinalizer(ctx context.Context) error { return nil } -const accessKeySecretNameTemplate = "%s-obj-key" - // GenerateKeySecret returns a secret suitable for submission to the Kubernetes API. // The secret is expected to contain keys for accessing the bucket, as well as owner and controller references. func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *linodego.ObjectStorageKey) (*corev1.Secret, error) { @@ -108,7 +106,6 @@ func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *lino return nil, errors.New("expected non-nil object storage key") } - secretName := fmt.Sprintf(accessKeySecretNameTemplate, s.Key.Name) secretStringData := make(map[string]string) tmplData := map[string]string{ @@ -118,7 +115,7 @@ func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *lino // If the desired secret is of ClusterResourceSet type, encapsulate the secret. // Bucket details are retrieved from the first referenced LinodeObjectStorageBucket in the access key. - if s.Key.Spec.SecretType == clusteraddonsv1.ClusterResourceSetSecretType { + if s.Key.Spec.GeneratedSecret.Type == clusteraddonsv1.ClusterResourceSetSecretType { // This should never run since the CRD has a validation marker to ensure bucketAccess has at least one item. if len(s.Key.Spec.BucketAccess) == 0 { return nil, fmt.Errorf("unable to generate %s; spec.bucketAccess must not be empty", clusteraddonsv1.ClusterResourceSetSecretType) @@ -131,14 +128,14 @@ func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *lino } tmplData["BucketEndpoint"] = bucket.Hostname - } else if len(s.Key.Spec.SecretDataFormat) == 0 { + } else if len(s.Key.Spec.GeneratedSecret.Format) == 0 { secretStringData = map[string]string{ "access_key": key.AccessKey, "secret_key": key.SecretKey, } } - for key, tmpl := range s.Key.Spec.SecretDataFormat { + for key, tmpl := range s.Key.Spec.GeneratedSecret.Format { goTmpl, err := template.New(key).Parse(tmpl) if err != nil { return nil, fmt.Errorf("unable to generate secret; failed to parse template in secret data format for key %s: %w", key, err) @@ -154,19 +151,19 @@ func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *lino secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: secretName, - Namespace: s.Key.Namespace, + Name: s.Key.Spec.GeneratedSecret.Name, + Namespace: s.Key.Spec.GeneratedSecret.Namespace, }, - Type: s.Key.Spec.SecretType, + Type: s.Key.Spec.GeneratedSecret.Type, StringData: secretStringData, } - scheme := s.Client.Scheme() - if err := controllerutil.SetOwnerReference(s.Key, &secret, scheme); err != nil { - return nil, fmt.Errorf("could not set owner ref on access key secret %s: %w", secretName, err) - } - if err := controllerutil.SetControllerReference(s.Key, &secret, scheme); err != nil { - return nil, fmt.Errorf("could not set controller ref on access key secret %s: %w", secretName, err) + // Set an owner reference on a Secret if it will exist in the same namespace as the Key resource. + // Kubernetes does not allow cross-namespace ownership so modifications to a Secret in another namespace won't trigger reconciliation. + if s.Key.Spec.GeneratedSecret.Namespace == s.Key.Namespace { + if err := controllerutil.SetControllerReference(s.Key, &secret, s.Client.Scheme()); err != nil { + return nil, fmt.Errorf("could not set controller ref on access key secret %s/%s: %w", s.Key.Spec.GeneratedSecret.Name, s.Key.Spec.GeneratedSecret.Namespace, err) + } } return &secret, nil diff --git a/cloud/scope/object_storage_key_test.go b/cloud/scope/object_storage_key_test.go index a908a58df..4f0a8a70b 100644 --- a/cloud/scope/object_storage_key_test.go +++ b/cloud/scope/object_storage_key_test.go @@ -314,6 +314,12 @@ func TestGenerateKeySecret(t *testing.T) { Name: "test-key", Namespace: "test-namespace", }, + Spec: infrav1alpha2.LinodeObjectStorageKeySpec{ + GeneratedSecret: infrav1alpha2.GeneratedSecret{ + Name: "test-key-obj-key", + Namespace: "test-namespace", + }, + }, }, key: &linodego.ObjectStorageKey{ ID: 1, @@ -349,8 +355,12 @@ func TestGenerateKeySecret(t *testing.T) { Namespace: "test-namespace", }, Spec: infrav1alpha2.LinodeObjectStorageKeySpec{ - SecretDataFormat: map[string]string{ - "key": "{{ .AccessKey", + GeneratedSecret: infrav1alpha2.GeneratedSecret{ + Name: "test-key-obj-key", + Namespace: "test-namespace", + Format: map[string]string{ + "key": "{{ .AccessKey", + }, }, }, }, @@ -384,9 +394,13 @@ func TestGenerateKeySecret(t *testing.T) { Permissions: "read_write", }, }, - SecretType: clusteraddonsv1.ClusterResourceSetSecretType, - SecretDataFormat: map[string]string{ - "key": "{{ .AccessKey }},{{ .SecretKey }},{{ .BucketEndpoint }}", + GeneratedSecret: infrav1alpha2.GeneratedSecret{ + Name: "test-key-obj-key", + Namespace: "test-namespace", + Type: clusteraddonsv1.ClusterResourceSetSecretType, + Format: map[string]string{ + "key": "{{ .AccessKey }},{{ .SecretKey }},{{ .BucketEndpoint }}", + }, }, }, }, @@ -437,9 +451,13 @@ func TestGenerateKeySecret(t *testing.T) { Permissions: "read_write", }, }, - SecretType: clusteraddonsv1.ClusterResourceSetSecretType, - SecretDataFormat: map[string]string{ - "key": "{{ .AccessKey }},{{ .SecretKey }},{{ .BucketEndpoint }}", + GeneratedSecret: infrav1alpha2.GeneratedSecret{ + Name: "test-key-obj-key", + Namespace: "test-namespace", + Type: clusteraddonsv1.ClusterResourceSetSecretType, + Format: map[string]string{ + "key": "{{ .AccessKey }},{{ .SecretKey }},{{ .BucketEndpoint }}", + }, }, }, }, @@ -469,9 +487,13 @@ func TestGenerateKeySecret(t *testing.T) { Namespace: "test-namespace", }, Spec: infrav1alpha2.LinodeObjectStorageKeySpec{ - SecretType: clusteraddonsv1.ClusterResourceSetSecretType, - SecretDataFormat: map[string]string{ - "key": "{{ .AccessKey }},{{ .SecretKey }},{{ .BucketEndpoint }}", + GeneratedSecret: infrav1alpha2.GeneratedSecret{ + Name: "test-key-obj-key", + Namespace: "test-namespace", + Type: clusteraddonsv1.ClusterResourceSetSecretType, + Format: map[string]string{ + "key": "{{ .AccessKey }},{{ .SecretKey }},{{ .BucketEndpoint }}", + }, }, }, }, @@ -507,6 +529,12 @@ func TestGenerateKeySecret(t *testing.T) { Name: "test-key", Namespace: "test-namespace", }, + Spec: infrav1alpha2.LinodeObjectStorageKeySpec{ + GeneratedSecret: infrav1alpha2.GeneratedSecret{ + Name: "test-key-obj-key", + Namespace: "test-namespace", + }, + }, }, key: &linodego.ObjectStorageKey{ ID: 1, @@ -524,7 +552,7 @@ func TestGenerateKeySecret(t *testing.T) { expectK8s: func(mock *mock.MockK8sClient) { mock.EXPECT().Scheme().Return(runtime.NewScheme()) }, - expectedErr: fmt.Errorf("could not set owner ref on access key secret"), + expectedErr: fmt.Errorf("could not set controller ref on access key secret"), }, } for _, tt := range tests { diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragekeys.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragekeys.yaml index 53b010214..6e3ec5618 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragekeys.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_linodeobjectstoragekeys.yaml @@ -92,6 +92,38 @@ spec: type: string type: object x-kubernetes-map-type: atomic + generatedSecret: + description: GeneratedSecret configures the Secret to generate containing + access key details. + properties: + format: + additionalProperties: + type: string + description: |- + How to format the data stored in the generated Secret. + It supports Go template syntax and interpolating the following values: .AccessKey, .SecretKey. + If no format is supplied then a generic one is used containing the values specified. + When SecretType is set to addons.cluster.x-k8s.io/resource-set, a .BucketEndpoint value is also available pointing to the location of the first bucket specified in BucketAccess. + type: object + name: + description: The name of the generated Secret. If not set, the + name is formatted as "{name-of-obj-key}-obj-key". + type: string + namespace: + description: The namespace for the generated Secret. If not set, + defaults to the namespace of the LinodeObjectStorageKey. + type: string + type: + default: Opaque + description: The type of the generated Secret. + enum: + - Opaque + - addons.cluster.x-k8s.io/resource-set + type: string + type: object + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf keyGeneration: default: 0 description: KeyGeneration may be modified to trigger a rotation of @@ -102,26 +134,19 @@ spec: type: string description: |- SecretDataFormat instructs the controller how to format the data stored in the secret containing access key details. - It supports Go template syntax and interpolating the following values: .AccessKey, .SecretKey. - If no format is supplied then a generic one is used containing the values specified. - When SecretType is set to addons.cluster.x-k8s.io/resource-set, a .BucketEndpoint value is also available pointing to the location of the first bucket specified in BucketAccess. + Deprecated: Use generatedSecret.format. type: object - x-kubernetes-validations: - - message: Value is immutable - rule: self == oldSelf secretType: - default: Opaque - description: SecretType instructs the controller what type of secret - to generate containing access key details. + description: |- + SecretType instructs the controller what type of secret to generate containing access key details. + Deprecated: Use generatedSecret.type. enum: - Opaque - addons.cluster.x-k8s.io/resource-set type: string - x-kubernetes-validations: - - message: Value is immutable - rule: self == oldSelf required: - bucketAccess + - generatedSecret - keyGeneration type: object status: @@ -194,10 +219,6 @@ spec: default: false description: Ready denotes that the key has been provisioned. type: boolean - secretName: - description: SecretName specifies the name of the Secret containing - access key data. - type: string type: object type: object served: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index f6ce0eb53..9f2a84c00 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -1,5 +1,31 @@ --- apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-infrastructure-cluster-x-k8s-io-v1alpha2-linodeobjectstoragekey + failurePolicy: Fail + name: mutation.linodeobjectstoragekey.infrastructure.cluster.x-k8s.io + rules: + - apiGroups: + - infrastructure.cluster.x-k8s.io + apiVersions: + - v1alpha2 + operations: + - CREATE + - UPDATE + resources: + - linodeobjectstoragekeys + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration diff --git a/controller/linodeobjectstoragekey_controller.go b/controller/linodeobjectstoragekey_controller.go index 24281c4d5..cccffa5d0 100644 --- a/controller/linodeobjectstoragekey_controller.go +++ b/controller/linodeobjectstoragekey_controller.go @@ -173,11 +173,11 @@ func (r *LinodeObjectStorageKeyReconciler) reconcileApply(ctx context.Context, k r.Recorder.Event(keyScope.Key, corev1.EventTypeNormal, "KeyAssigned", "Object storage key assigned") // Ensure the generated secret still exists - case keyScope.Key.Status.AccessKeyRef != nil && keyScope.Key.Status.SecretName != nil: + case keyScope.Key.Status.AccessKeyRef != nil: secret := &corev1.Secret{} key := client.ObjectKey{ - Namespace: keyScope.Key.Namespace, - Name: *keyScope.Key.Status.SecretName, + Namespace: keyScope.Key.Spec.GeneratedSecret.Namespace, + Name: keyScope.Key.Spec.GeneratedSecret.Name, } if err := keyScope.Client.Get(ctx, key, secret); err != nil { @@ -213,7 +213,7 @@ func (r *LinodeObjectStorageKeyReconciler) reconcileApply(ctx context.Context, k emptySecret := &corev1.Secret{ObjectMeta: secret.ObjectMeta} operation, err := controllerutil.CreateOrUpdate(ctx, keyScope.Client, emptySecret, func() error { - emptySecret.Type = keyScope.Key.Spec.SecretType + emptySecret.Type = keyScope.Key.Spec.GeneratedSecret.Type emptySecret.StringData = secret.StringData emptySecret.Data = nil @@ -226,9 +226,7 @@ func (r *LinodeObjectStorageKeyReconciler) reconcileApply(ctx context.Context, k return err } - keyScope.Key.Status.SecretName = util.Pointer(secret.Name) - - keyScope.Logger.Info(fmt.Sprintf("Secret %s was %s with access key", secret.Name, operation)) + keyScope.Logger.Info(fmt.Sprintf("Secret %s/%s was %s with access key", secret.Namespace, secret.Name, operation)) r.Recorder.Event(keyScope.Key, corev1.EventTypeNormal, "KeyStored", "Object storage key stored in secret") } @@ -253,6 +251,23 @@ func (r *LinodeObjectStorageKeyReconciler) reconcileDelete(ctx context.Context, r.Recorder.Event(keyScope.Key, clusterv1.DeletedReason, "KeyRevoked", "Object storage key revoked") + // If this key's Secret was generated in another namespace, manually delete it since it has no owner reference. + if keyScope.Key.Spec.GeneratedSecret.Namespace != keyScope.Key.Namespace { + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: keyScope.Key.Spec.GeneratedSecret.Name, + Namespace: keyScope.Key.Spec.GeneratedSecret.Namespace, + }, + } + if err := keyScope.Client.Delete(ctx, &secret); err != nil { + err := errors.New("failed to delete generated secret; unable to delete") + keyScope.Logger.Error(err, "client.Delete") + r.setFailure(keyScope, err) + + return err + } + } + if !controllerutil.RemoveFinalizer(keyScope.Key, infrav1alpha2.ObjectStorageKeyFinalizer) { err := errors.New("failed to remove finalizer from key; unable to delete") keyScope.Logger.Error(err, "controllerutil.RemoveFinalizer") diff --git a/controller/linodeobjectstoragekey_controller_test.go b/controller/linodeobjectstoragekey_controller_test.go index ebecea9ac..b3ef2ae5d 100644 --- a/controller/linodeobjectstoragekey_controller_test.go +++ b/controller/linodeobjectstoragekey_controller_test.go @@ -53,6 +53,10 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { Namespace: "default", }, Spec: infrav1.LinodeObjectStorageKeySpec{ + GeneratedSecret: infrav1.GeneratedSecret{ + Name: "lifecycle-obj-key", + Namespace: "default", + }, BucketAccess: []infrav1.BucketAccessRef{ { BucketName: "mybucket", @@ -128,7 +132,7 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { By("secret") var secret corev1.Secret - secretKey := client.ObjectKey{Namespace: "default", Name: *key.Status.SecretName} + secretKey := client.ObjectKey{Namespace: "default", Name: "lifecycle-obj-key"} Expect(k8sClient.Get(ctx, secretKey, &secret)).To(Succeed()) Expect(secret.Data).To(HaveLen(2)) Expect(string(secret.Data["access_key"])).To(Equal("access-key-1")) @@ -141,7 +145,7 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { logOutput := mck.Logs() Expect(logOutput).To(ContainSubstring("Reconciling apply")) - Expect(logOutput).To(ContainSubstring("Secret %s was created with access key", *key.Status.SecretName)) + Expect(logOutput).To(ContainSubstring("Secret default/lifecycle-obj-key was created with access key")) }), ), ), @@ -183,7 +187,7 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { By("secret") var secret corev1.Secret - secretKey := client.ObjectKey{Namespace: "default", Name: *key.Status.SecretName} + secretKey := client.ObjectKey{Namespace: "default", Name: "lifecycle-obj-key"} Expect(k8sClient.Get(ctx, secretKey, &secret)).To(Succeed()) Expect(secret.Data).To(HaveLen(2)) Expect(string(secret.Data["access_key"])).To(Equal("access-key-2")) @@ -196,13 +200,13 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { logOutput := mck.Logs() Expect(logOutput).To(ContainSubstring("Reconciling apply")) - Expect(logOutput).To(ContainSubstring("Secret %s was updated with access key", *key.Status.SecretName)) + Expect(logOutput).To(ContainSubstring("Secret default/lifecycle-obj-key was updated with access key")) }), ), ), Once("secret is deleted", func(ctx context.Context, _ Mock) { var secret corev1.Secret - secretKey := client.ObjectKey{Namespace: "default", Name: *key.Status.SecretName} + secretKey := client.ObjectKey{Namespace: "default", Name: "lifecycle-obj-key"} Expect(k8sClient.Get(ctx, secretKey, &secret)).To(Succeed()) Expect(k8sClient.Delete(ctx, &secret)).To(Succeed()) }), @@ -232,7 +236,7 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { Expect(err).NotTo(HaveOccurred()) var secret corev1.Secret - secretKey := client.ObjectKey{Namespace: "default", Name: *key.Status.SecretName} + secretKey := client.ObjectKey{Namespace: "default", Name: "lifecycle-obj-key"} Expect(k8sClient.Get(ctx, secretKey, &secret)).To(Succeed()) Expect(secret.Data).To(HaveLen(2)) Expect(string(secret.Data["access_key"])).To(Equal("access-key-2")) @@ -245,12 +249,12 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { logOutput := mck.Logs() Expect(logOutput).To(ContainSubstring("Reconciling apply")) - Expect(logOutput).To(ContainSubstring("Secret %s was created with access key", *key.Status.SecretName)) + Expect(logOutput).To(ContainSubstring("Secret default/lifecycle-obj-key was created with access key")) }), ), ), - Once("secretType set to cluster resource set fails", func(ctx context.Context, _ Mock) { - key.Spec.SecretType = clusteraddonsv1.ClusterResourceSetSecretType + Once("secret type set to cluster resource set fails", func(ctx context.Context, _ Mock) { + key.Spec.GeneratedSecret.Type = clusteraddonsv1.ClusterResourceSetSecretType Expect(k8sClient.Update(ctx, &key)).NotTo(Succeed()) }), Once("resource is deleted", func(ctx context.Context, _ Mock) { @@ -293,7 +297,7 @@ var _ = Describe("lifecycle", Ordered, Label("key", "key-lifecycle"), func() { ) }) -var _ = Describe("secret-template", Label("key", "key-secret-template"), func() { +var _ = Describe("custom-secret", Label("key", "key-custom-secret"), func() { suite := NewControllerSuite(GinkgoT(), mock.MockLinodeClient{}) reconciler := LinodeObjectStorageKeyReconciler{} @@ -309,6 +313,9 @@ var _ = Describe("secret-template", Label("key", "key-secret-template"), func() Namespace: "default", }, Spec: infrav1.LinodeObjectStorageKeySpec{ + GeneratedSecret: infrav1.GeneratedSecret{ + Namespace: "other", + }, BucketAccess: []infrav1.BucketAccessRef{ { BucketName: "mybucket", @@ -321,6 +328,9 @@ var _ = Describe("secret-template", Label("key", "key-secret-template"), func() }) suite.Run( + Once("create other namespace", func(ctx context.Context, _ Mock) { + Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "other"}})).To(Succeed()) + }), Call("key created", func(ctx context.Context, mck Mock) { mck.LinodeClient.EXPECT().CreateObjectStorageKey(gomock.Any(), gomock.Any()).Return(&linodego.ObjectStorageKey{ ID: 1, @@ -340,8 +350,9 @@ var _ = Describe("secret-template", Label("key", "key-secret-template"), func() Call("with opaque secret", func(ctx context.Context, mck Mock) { keyScope.LinodeClient = mck.LinodeClient keyScope.Key.ObjectMeta.Name = "opaque" - keyScope.Key.Spec.SecretType = corev1.SecretTypeOpaque - keyScope.Key.Spec.SecretDataFormat = map[string]string{ + keyScope.Key.Spec.GeneratedSecret.Name = "opaque-custom-secret" + keyScope.Key.Spec.GeneratedSecret.Type = corev1.SecretTypeOpaque + keyScope.Key.Spec.GeneratedSecret.Format = map[string]string{ "data": "{{ .AccessKey }}-{{ .SecretKey }}", } @@ -355,7 +366,7 @@ var _ = Describe("secret-template", Label("key", "key-secret-template"), func() Expect(err).NotTo(HaveOccurred()) var secret corev1.Secret - secretKey := client.ObjectKey{Namespace: "default", Name: "opaque-obj-key"} + secretKey := client.ObjectKey{Namespace: "other", Name: "opaque-custom-secret"} Expect(k8sClient.Get(ctx, secretKey, &secret)).To(Succeed()) Expect(secret.Data).To(HaveLen(1)) Expect(string(secret.Data["data"])).To(Equal("access-key-secret-key")) @@ -365,8 +376,9 @@ var _ = Describe("secret-template", Label("key", "key-secret-template"), func() Call("with cluster-resource-set secret", func(ctx context.Context, mck Mock) { keyScope.LinodeClient = mck.LinodeClient keyScope.Key.ObjectMeta.Name = "cluster-resource-set" - keyScope.Key.Spec.SecretType = clusteraddonsv1.ClusterResourceSetSecretType - keyScope.Key.Spec.SecretDataFormat = map[string]string{ + keyScope.Key.Spec.GeneratedSecret.Name = "cluster-resource-set-custom-secret" + keyScope.Key.Spec.GeneratedSecret.Type = clusteraddonsv1.ClusterResourceSetSecretType + keyScope.Key.Spec.GeneratedSecret.Format = map[string]string{ "data": "{{ .AccessKey }}-{{ .SecretKey }}-{{ .BucketEndpoint }}", } @@ -384,7 +396,7 @@ var _ = Describe("secret-template", Label("key", "key-secret-template"), func() Expect(err).NotTo(HaveOccurred()) var secret corev1.Secret - secretKey := client.ObjectKey{Namespace: "default", Name: "cluster-resource-set-obj-key"} + secretKey := client.ObjectKey{Namespace: "other", Name: "cluster-resource-set-custom-secret"} Expect(k8sClient.Get(ctx, secretKey, &secret)).To(Succeed()) Expect(secret.Data).To(HaveLen(1)) Expect(string(secret.Data["data"])).To(Equal("access-key-secret-key-hostname")) @@ -416,6 +428,10 @@ var _ = Describe("errors", Label("key", "key-errors"), func() { Namespace: "default", }, Spec: infrav1.LinodeObjectStorageKeySpec{ + GeneratedSecret: infrav1.GeneratedSecret{ + Name: "mock-obj-key", + Namespace: "default", + }, BucketAccess: []infrav1.BucketAccessRef{ { BucketName: "mybucket", @@ -490,7 +506,6 @@ var _ = Describe("errors", Label("key", "key-errors"), func() { Result("error", func(ctx context.Context, mck Mock) { keyScope.Key.Spec.KeyGeneration = 1 keyScope.Key.Status.LastKeyGeneration = ptr.To(keyScope.Key.Spec.KeyGeneration) - keyScope.Key.Status.SecretName = ptr.To("mock-obj-key") keyScope.Key.Status.AccessKeyRef = ptr.To(1) keyScope.LinodeClient = mck.LinodeClient @@ -518,7 +533,6 @@ var _ = Describe("errors", Label("key", "key-errors"), func() { Result("creation error", func(ctx context.Context, mck Mock) { keyScope.Key.Spec.KeyGeneration = 1 keyScope.Key.Status.LastKeyGeneration = ptr.To(keyScope.Key.Spec.KeyGeneration) - keyScope.Key.Status.SecretName = ptr.To("mock-obj-key") keyScope.Key.Status.AccessKeyRef = ptr.To(1) keyScope.LinodeClient = mck.LinodeClient @@ -537,7 +551,6 @@ var _ = Describe("errors", Label("key", "key-errors"), func() { Result("error", func(ctx context.Context, mck Mock) { keyScope.Key.Spec.KeyGeneration = 1 keyScope.Key.Status.LastKeyGeneration = ptr.To(keyScope.Key.Spec.KeyGeneration) - keyScope.Key.Status.SecretName = ptr.To("mock-obj-key") keyScope.Key.Status.AccessKeyRef = ptr.To(1) keyScope.LinodeClient = mck.LinodeClient diff --git a/docs/src/topics/backups.md b/docs/src/topics/backups.md index 33bb8d911..1e2f261c3 100644 --- a/docs/src/topics/backups.md +++ b/docs/src/topics/backups.md @@ -20,7 +20,7 @@ For more fine-grain control and to know more about etcd backups, refer to [the b ## Object Storage -Additionally, CAPL can be used to provision Object Storage buckets and access keys for general purposes by configuring a `LinodeObjectStorageBucket` resource. +Additionally, CAPL can be used to provision Object Storage buckets and access keys for general purposes by configuring `LinodeObjectStorageBucket` and `LinodeObjectStorageKey` resources. ```admonish warning Using this feature requires enabling Object Storage in the account where the resources will be provisioned. Please refer to the [Pricing](https://www.linode.com/docs/products/storage/object-storage/#pricing) information in Linode's [Object Storage documentation](https://www.linode.com/docs/products/storage/object-storage/). @@ -28,7 +28,7 @@ Using this feature requires enabling Object Storage in the account where the res ### Bucket Creation -The following is the minimal required configuration needed to provision an Object Storage bucket and set of access keys. +The following is the minimal required configuration needed to provision an Object Storage bucket. ```yaml apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 @@ -38,7 +38,6 @@ metadata: namespace: spec: region: - secretType: Opaque ``` Upon creation of the resource, CAPL will provision a bucket in the region specified using the `.metadata.name` as the bucket's label. @@ -47,9 +46,50 @@ Upon creation of the resource, CAPL will provision a bucket in the region specif The bucket label must be unique within the region across all accounts. Otherwise, CAPL will populate the resource status fields with errors to show that the operation failed. ``` -### Access Keys Creation +### Bucket Status -CAPL will also create `read_write` and `read_only` access keys for the bucket and store credentials in a secret in the same namespace where the `LinodeObjectStorageBucket` was created along with other details about the Linode OBJ Bucket: +Upon successful provisioning of a bucket, the `LinodeObjectStorageBucket` resource's status will resemble the following: + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageBucket +metadata: + name: + namespace: +spec: + region: +status: + ready: true + conditions: + - type: Ready + status: "True" + lastTransitionTime: + hostname: + creationTime: +``` + +### Access Key Creation + +The following is the minimal required configuration needed to provision an Object Storage key. + +```yaml +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: + namespace: +spec: + bucketAccess: + - bucketName: + permissions: read_only + region: + generatedSecret: + type: Opaque +``` + +Upon creation of the resource, CAPL will provision an access key in the region specified using the `.metadata.name` as the key's label. + +The credentials for the provisioned access key will be stored in a Secret. By default, the Secret is generated in the same namespace as the `LinodeObjectStorageKey`: ```yaml apiVersion: v1 @@ -64,62 +104,62 @@ metadata: controller: true uid: data: - bucket_name: - bucket_region: - bucket_endpoint: access_key: secret_key: ``` -The-obj-key secret is owned and managed by CAPL during the life of the `LinodeObjectStorageBucket`. +The secret is owned and managed by CAPL during the life of the `LinodeObjectStorageBucket`. -### Access Keys Rotation +### Access Key Status -The following configuration with `keyGeneration` set to a new value (different from `.status.lastKeyGeneration`) will instruct CAPL to rotate the access keys. +Upon successful provisioning of a key, the `LinodeObjectStorageKey` resource's status will resemble the following: ```yaml apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 -kind: LinodeObjectStorageBucket +kind: LinodeObjectStorageKey metadata: - name: + name: namespace: spec: - cluster: - secretType: Opaque - keyGeneration: 1 -# status: -# lastKeyGeneration: 0 + bucketAccess: + - bucketName: + permissions: read_only + region: + generatedSecret: + type: Opaque +status: + ready: true + conditions: + - type: Ready + status: "True" + lastTransitionTime: + accessKeyRef: + creationTime: + lastKeyGeneration: 0 ``` -### Bucket Status +### Access Key Rotation -Upon successful provisioning of a bucket and keys, the `LinodeObjectStorageBucket` resource's status will resemble the following: +The following configuration with `keyGeneration` set to a new value (different from `.status.lastKeyGeneration`) will instruct CAPL to rotate the access key. ```yaml apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 -kind: LinodeObjectStorageBucket +kind: LinodeObjectStorageKey metadata: - name: + name: namespace: spec: - cluster: - secretType: Opaque - keyGeneration: 0 -status: - ready: true - conditions: - - type: Ready - status: "True" - lastTransitionTime: - hostname: - creationTime: - lastKeyGeneration: 0 - keySecretName: -bucket-details - accessKeyRefs: - - - - + bucketAccess: + - bucketName: + permissions: read_only + region: + generatedSecret: + type: Opaque + keyGeneration: 1 +# status: +# lastKeyGeneration: 0 ``` ### Resource Deletion -When deleting a `LinodeObjectStorageBucket` resource, CAPL will deprovision the access keys and managed secret but retain the underlying bucket to avoid unintended data loss. +When deleting a `LinodeObjectStorageKey` resource, CAPL will deprovision the access key and delete the managed secret. However, when deleting a `LinodeObjectStorageBucket` resource, CAPL will retain the underlying bucket to avoid unintended data loss. diff --git a/docs/src/topics/multi-tenancy.md b/docs/src/topics/multi-tenancy.md index c67b0b949..e059c603b 100644 --- a/docs/src/topics/multi-tenancy.md +++ b/docs/src/topics/multi-tenancy.md @@ -61,6 +61,16 @@ spec: credentialsRef: name: linode-credentials ... +--- +# Example: LinodeObjectStorageKey +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: test-key +spec: + credentialsRef: + name: linode-credentials + ... ``` Secrets from other namespaces by additionally specifying an optional diff --git a/e2e/linodeobjectstoragekey-controller/custom-secret/assert-capi-resources.yaml b/e2e/linodeobjectstoragekey-controller/custom-secret/assert-capi-resources.yaml new file mode 100644 index 000000000..8a5b8dbab --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/custom-secret/assert-capi-resources.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capl-controller-manager + namespace: capl-system +status: + availableReplicas: 1 diff --git a/e2e/linodeobjectstoragekey-controller/custom-secret/assert-key-and-secret.yaml b/e2e/linodeobjectstoragekey-controller/custom-secret/assert-key-and-secret.yaml new file mode 100644 index 000000000..866ed79ee --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/custom-secret/assert-key-and-secret.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: ($key) +spec: + keyGeneration: 0 +status: + ready: true + lastKeyGeneration: 0 +--- +apiVersion: v1 +kind: Secret +metadata: + name: ($access_key_secret) + namespace: default +data: + (the_access_key != null): true + (the_secret_key != null): true diff --git a/e2e/linodeobjectstoragekey-controller/custom-secret/chainsaw-test.yaml b/e2e/linodeobjectstoragekey-controller/custom-secret/chainsaw-test.yaml new file mode 100755 index 000000000..1b0f98af3 --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/custom-secret/chainsaw-test.yaml @@ -0,0 +1,108 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: key-custom-secret + # Label to trigger the test on every PR + labels: + all: + quick: + linodeobjkey: + linodeobjkeysecret: +spec: + bindings: + # A short identifier for the E2E test run + - name: run + value: (join('-', ['e2e', 'key-cust-secret', env('GIT_REF')])) + - name: key + # Format the key name into a valid Kubernetes object name + # TODO: This is over-truncated to account for the Kubernetes access key Secret + value: (trim((truncate(($run), `52`)), '-')) + - name: access_key_secret + value: (join('-', [($key), 'custom'])) + template: true + steps: + - name: Check if CAPI provider resources exist + try: + - assert: + file: assert-capi-resources.yaml + - name: Create bucket + try: + - script: + env: + - name: URI + value: object-storage/buckets + - name: BUCKET_LABEL + value: ($key) + content: | + set -e + + curl -s \ + -X POST \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"label\":\"$BUCKET_LABEL\",\"region\":\"us-sea\"}" \ + "https://api.linode.com/v4/$URI" + check: + ($error): ~ + - name: Create LinodeObjectStorageKey + try: + - apply: + file: create-linodeobjectstoragekey.yaml + - assert: + file: assert-key-and-secret.yaml + catch: + - describe: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 + kind: LinodeObjectStorageKey + - describe: + apiVersion: v1 + kind: Secret + namespace: default + - name: Ensure the access key was created + try: + - script: + env: + - name: URI + value: object-storage/keys + - name: OBJ_KEY + value: ($key) + content: | + set -e + + export KEY_ID=$(kubectl -n $NAMESPACE get lobjkey $OBJ_KEY -ojson | jq '.status.accessKeyRef') + + curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.linode.com/v4/$URI/$KEY_ID" + check: + ($error): ~ + - name: Delete LinodeObjectStorageKey + try: + - delete: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 + kind: LinodeObjectStorageKey + name: ($key) + - name: Check if the LinodeObjectStorageKey and Secret were deleted + try: + - error: + file: check-key-and-secret-deletion.yaml + - name: Delete bucket + try: + - script: + env: + - name: URI + value: object-storage/buckets/us-sea + - name: BUCKET_LABEL + value: ($key) + content: | + set -e + + curl -s \ + -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "https://api.linode.com/v4/$URI/$BUCKET_LABEL" + check: + ($error): ~ \ No newline at end of file diff --git a/e2e/linodeobjectstoragekey-controller/custom-secret/check-key-and-secret-deletion.yaml b/e2e/linodeobjectstoragekey-controller/custom-secret/check-key-and-secret-deletion.yaml new file mode 100644 index 000000000..7b03d61e6 --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/custom-secret/check-key-and-secret-deletion.yaml @@ -0,0 +1,10 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: ($key) +--- +apiVersion: v1 +kind: Secret +metadata: + name: ($access_key_secret) + namespace: default \ No newline at end of file diff --git a/e2e/linodeobjectstoragekey-controller/custom-secret/create-linodeobjectstoragekey.yaml b/e2e/linodeobjectstoragekey-controller/custom-secret/create-linodeobjectstoragekey.yaml new file mode 100644 index 000000000..b896d2e21 --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/custom-secret/create-linodeobjectstoragekey.yaml @@ -0,0 +1,15 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: ($key) +spec: + bucketAccess: + - bucketName: ($key) + permissions: read_only + region: us-sea + generatedSecret: + name: ($access_key_secret) + namespace: default + format: + the_access_key: "{{ .AccessKey }}" + the_secret_key: "{{ .SecretKey }}" diff --git a/e2e/linodeobjectstoragekey-controller/deprecated-secret/assert-capi-resources.yaml b/e2e/linodeobjectstoragekey-controller/deprecated-secret/assert-capi-resources.yaml new file mode 100644 index 000000000..8a5b8dbab --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/deprecated-secret/assert-capi-resources.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capi-controller-manager + namespace: capi-system +status: + availableReplicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: capl-controller-manager + namespace: capl-system +status: + availableReplicas: 1 diff --git a/e2e/linodeobjectstoragekey-controller/deprecated-secret/assert-key-and-secret.yaml b/e2e/linodeobjectstoragekey-controller/deprecated-secret/assert-key-and-secret.yaml new file mode 100644 index 000000000..866ed79ee --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/deprecated-secret/assert-key-and-secret.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: ($key) +spec: + keyGeneration: 0 +status: + ready: true + lastKeyGeneration: 0 +--- +apiVersion: v1 +kind: Secret +metadata: + name: ($access_key_secret) + namespace: default +data: + (the_access_key != null): true + (the_secret_key != null): true diff --git a/e2e/linodeobjectstoragekey-controller/deprecated-secret/chainsaw-test.yaml b/e2e/linodeobjectstoragekey-controller/deprecated-secret/chainsaw-test.yaml new file mode 100755 index 000000000..7d7a0a3e8 --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/deprecated-secret/chainsaw-test.yaml @@ -0,0 +1,108 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: deprecated-secret + # Label to trigger the test on every PR + labels: + all: + quick: + linodeobjkey: + linodeobjkeysecretdep: +spec: + bindings: + # A short identifier for the E2E test run + - name: run + value: (join('-', ['e2e', 'deprecated-secret', env('GIT_REF')])) + - name: key + # Format the key name into a valid Kubernetes object name + # TODO: This is over-truncated to account for the Kubernetes access key Secret + value: (trim((truncate(($run), `52`)), '-')) + - name: access_key_secret + value: (join('-', [($key), 'custom'])) + template: true + steps: + - name: Check if CAPI provider resources exist + try: + - assert: + file: assert-capi-resources.yaml + - name: Create bucket + try: + - script: + env: + - name: URI + value: object-storage/buckets + - name: BUCKET_LABEL + value: ($key) + content: | + set -e + + curl -s \ + -X POST \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"label\":\"$BUCKET_LABEL\",\"region\":\"us-sea\"}" \ + "https://api.linode.com/v4/$URI" + check: + ($error): ~ + - name: Create LinodeObjectStorageKey + try: + - apply: + file: create-linodeobjectstoragekey.yaml + - assert: + file: assert-key-and-secret.yaml + catch: + - describe: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 + kind: LinodeObjectStorageKey + - describe: + apiVersion: v1 + kind: Secret + namespace: default + - name: Ensure the access key was created + try: + - script: + env: + - name: URI + value: object-storage/keys + - name: OBJ_KEY + value: ($key) + content: | + set -e + + export KEY_ID=$(kubectl -n $NAMESPACE get lobjkey $OBJ_KEY -ojson | jq '.status.accessKeyRef') + + curl -s \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.linode.com/v4/$URI/$KEY_ID" + check: + ($error): ~ + - name: Delete LinodeObjectStorageKey + try: + - delete: + ref: + apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 + kind: LinodeObjectStorageKey + name: ($key) + - name: Check if the LinodeObjectStorageKey and Secret were deleted + try: + - error: + file: check-key-and-secret-deletion.yaml + - name: Delete bucket + try: + - script: + env: + - name: URI + value: object-storage/buckets/us-sea + - name: BUCKET_LABEL + value: ($key) + content: | + set -e + + curl -s \ + -X DELETE \ + -H "Authorization: Bearer $LINODE_TOKEN" \ + "https://api.linode.com/v4/$URI/$BUCKET_LABEL" + check: + ($error): ~ \ No newline at end of file diff --git a/e2e/linodeobjectstoragekey-controller/deprecated-secret/check-key-and-secret-deletion.yaml b/e2e/linodeobjectstoragekey-controller/deprecated-secret/check-key-and-secret-deletion.yaml new file mode 100644 index 000000000..7b03d61e6 --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/deprecated-secret/check-key-and-secret-deletion.yaml @@ -0,0 +1,10 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: ($key) +--- +apiVersion: v1 +kind: Secret +metadata: + name: ($access_key_secret) + namespace: default \ No newline at end of file diff --git a/e2e/linodeobjectstoragekey-controller/deprecated-secret/create-linodeobjectstoragekey.yaml b/e2e/linodeobjectstoragekey-controller/deprecated-secret/create-linodeobjectstoragekey.yaml new file mode 100644 index 000000000..58ee27232 --- /dev/null +++ b/e2e/linodeobjectstoragekey-controller/deprecated-secret/create-linodeobjectstoragekey.yaml @@ -0,0 +1,16 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1alpha2 +kind: LinodeObjectStorageKey +metadata: + name: ($key) +spec: + bucketAccess: + - bucketName: ($key) + permissions: read_only + region: us-sea + secretType: Opaque + secretDataFormat: + the_access_key: "{{ .AccessKey }}" + the_secret_key: "{{ .SecretKey }}" + generatedSecret: + name: ($access_key_secret) + namespace: default diff --git a/e2e/linodeobjectstoragekey-controller/minimal-linodeobjectstoragekey/assert-key-and-secret.yaml b/e2e/linodeobjectstoragekey-controller/minimal-linodeobjectstoragekey/assert-key-and-secret.yaml index 6629be44a..384ce6662 100644 --- a/e2e/linodeobjectstoragekey-controller/minimal-linodeobjectstoragekey/assert-key-and-secret.yaml +++ b/e2e/linodeobjectstoragekey-controller/minimal-linodeobjectstoragekey/assert-key-and-secret.yaml @@ -11,7 +11,6 @@ spec: keyGeneration: 0 status: ready: true - secretName: ($access_key_secret) lastKeyGeneration: 0 --- apiVersion: v1 diff --git a/templates/addons/etcd-backup-restore/linode-obj.yaml b/templates/addons/etcd-backup-restore/linode-obj.yaml index bed9918bf..3d9438112 100644 --- a/templates/addons/etcd-backup-restore/linode-obj.yaml +++ b/templates/addons/etcd-backup-restore/linode-obj.yaml @@ -29,20 +29,21 @@ spec: - bucketName: ${CLUSTER_NAME}-etcd-backup permissions: read_write region: ${OBJ_BUCKET_REGION:=${LINODE_REGION}} - secretType: addons.cluster.x-k8s.io/resource-set - secretDataFormat: - etcd-backup.yaml: | - apiVersion: v1 - kind: Secret - metadata: - name: ${CLUSTER_NAME}-etcd-backup-obj-key - namespace: kube-system - stringData: - bucket_name: ${CLUSTER_NAME}-etcd-backup - bucket_region: ${OBJ_BUCKET_REGION:=${LINODE_REGION}} - bucket_endpoint: {{ .BucketEndpoint }} - access_key: {{ .AccessKey }} - secret_key: {{ .SecretKey }} + generatedSecret: + type: addons.cluster.x-k8s.io/resource-set + format: + etcd-backup.yaml: | + apiVersion: v1 + kind: Secret + metadata: + name: ${CLUSTER_NAME}-etcd-backup-obj-key + namespace: kube-system + stringData: + bucket_name: ${CLUSTER_NAME}-etcd-backup + bucket_region: ${OBJ_BUCKET_REGION:=${LINODE_REGION}} + bucket_endpoint: {{ .BucketEndpoint }} + access_key: {{ .AccessKey }} + secret_key: {{ .SecretKey }} --- apiVersion: addons.cluster.x-k8s.io/v1beta1 kind: ClusterResourceSet