Skip to content

Commit

Permalink
[feat] Support key secret templating (#476)
Browse files Browse the repository at this point in the history
  • Loading branch information
bcm820 authored Aug 23, 2024
1 parent 78eec8d commit f9e802e
Show file tree
Hide file tree
Showing 14 changed files with 519 additions and 402 deletions.
9 changes: 9 additions & 0 deletions api/v1alpha2/linodeobjectstoragekey_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,17 @@ type LinodeObjectStorageKeySpec struct {
// SecretType instructs the controller what type of secret to generate containing access key details.
// +kubebuilder:validation:Enum=Opaque;addons.cluster.x-k8s.io/resource-set
// +kubebuilder:default=Opaque
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +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"
// +optional
SecretDataFormat map[string]string `json:"secretDataFormat,omitempty"`
}

// LinodeObjectStorageKeyStatus defines the observed state of LinodeObjectStorageKey
Expand Down
83 changes: 83 additions & 0 deletions api/v1alpha2/linodeobjectstoragekey_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Copyright 2023 Akamai Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha2

import (
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
clusteraddonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var linodeobjectstoragekeylog = logf.Log.WithName("linodeobjectstoragekey-resource")

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *LinodeObjectStorageKey) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
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

var _ webhook.Validator = &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)

return r.validateLinodeObjectStorageKey()
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *LinodeObjectStorageKey) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
linodeobjectstoragekeylog.Info("validate update", "name", r.Name)

return r.validateLinodeObjectStorageKey()
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *LinodeObjectStorageKey) ValidateDelete() (admission.Warnings, error) {
return nil, nil
}

func (r *LinodeObjectStorageKey) validateLinodeObjectStorageKey() (admission.Warnings, error) {
var errs field.ErrorList

if r.Spec.SecretType == clusteraddonsv1.ClusterResourceSetSecretType && len(r.Spec.SecretDataFormat) == 0 {
errs = append(errs, field.Invalid(
field.NewPath("spec").Child("secretDataFormat"),
r.Spec.SecretDataFormat,
fmt.Sprintf("must not be empty with Secret type %s", clusteraddonsv1.ClusterResourceSetSecretType),
))
}

if len(errs) > 0 {
return nil, apierrors.NewInvalid(schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "LinodeObjectStorageKey"}, r.Name, errs)
}

return nil, nil
}
86 changes: 86 additions & 0 deletions api/v1alpha2/linodeobjectstoragekey_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
Copyright 2023 Akamai Technologies, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1alpha2

import (
"errors"
"strings"
"testing"

corev1 "k8s.io/api/core/v1"
clusteraddonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
)

func TestValidateLinodeObjectStorageKey(t *testing.T) {
t.Parallel()

tests := []struct {
name string
spec LinodeObjectStorageKeySpec
err error
}{
{
name: "opaque",
spec: LinodeObjectStorageKeySpec{
SecretType: corev1.SecretTypeOpaque,
},
err: nil,
},
{
name: "resourceset with empty secret data format",
spec: LinodeObjectStorageKeySpec{
SecretType: clusteraddonsv1.ClusterResourceSetSecretType,
SecretDataFormat: 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",
},
},
err: nil,
},
}

for _, tt := range tests {
testcase := tt

t.Run(testcase.name, func(t *testing.T) {
t.Parallel()

key := LinodeObjectStorageKey{
Spec: testcase.spec,
}

_, err := key.validateLinodeObjectStorageKey()
if err != nil {
if testcase.err == nil {
t.Fatal(err)
}
if errStr := testcase.err.Error(); !strings.Contains(err.Error(), errStr) {
t.Errorf("error did not contain substring '%s'", errStr)
}
} else if testcase.err != nil {
t.Fatal("expected an error")
}
})
}
}
7 changes: 7 additions & 0 deletions api/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 25 additions & 69 deletions cloud/scope/object_storage_key.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package scope

import (
"bytes"
"context"
"errors"
"fmt"
"text/template"

"github.com/go-logr/logr"
"github.com/linode/linodego"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clusteraddonsv1 "sigs.k8s.io/cluster-api/exp/addons/api/v1beta1"
"sigs.k8s.io/cluster-api/util/patch"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

infrav1alpha2 "github.com/linode/cluster-api-provider-linode/api/v1alpha2"
Expand Down Expand Up @@ -99,27 +99,7 @@ func (s *ObjectStorageKeyScope) AddFinalizer(ctx context.Context) error {
return nil
}

const (
accessKeySecretNameTemplate = "%s-obj-key"

ClusterResourceSetSecretFilename = "etcd-backup.yaml"
BucketKeySecret = `kind: Secret
apiVersion: v1
metadata:
name: %s
namespace: kube-system
stringData:
bucket_name: %s
bucket_region: %s
bucket_endpoint: %s
access_key: %s
secret_key: %s`
)

var secretTypeExpectedKey = map[corev1.SecretType]string{
corev1.SecretTypeOpaque: "access_key",
clusteraddonsv1.ClusterResourceSetSecretType: ClusterResourceSetSecretFilename,
}
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.
Expand All @@ -128,9 +108,13 @@ func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *lino
return nil, errors.New("expected non-nil object storage key")
}

var secretStringData map[string]string

secretName := fmt.Sprintf(accessKeySecretNameTemplate, s.Key.Name)
secretStringData := make(map[string]string)

tmplData := map[string]string{
"AccessKey": key.AccessKey,
"SecretKey": key.SecretKey,
}

// If the desired secret is of ClusterResourceSet type, encapsulate the secret.
// Bucket details are retrieved from the first referenced LinodeObjectStorageBucket in the access key.
Expand All @@ -146,24 +130,28 @@ func (s *ObjectStorageKeyScope) GenerateKeySecret(ctx context.Context, key *lino
return nil, fmt.Errorf("unable to generate %s; failed to get bucket: %w", clusteraddonsv1.ClusterResourceSetSecretType, err)
}

secretStringData = map[string]string{
ClusterResourceSetSecretFilename: fmt.Sprintf(
BucketKeySecret,
secretName,
bucket.Label,
bucket.Region,
bucket.Hostname,
key.AccessKey,
key.SecretKey,
),
}
} else {
tmplData["BucketEndpoint"] = bucket.Hostname
} else if len(s.Key.Spec.SecretDataFormat) == 0 {
secretStringData = map[string]string{
"access_key": key.AccessKey,
"secret_key": key.SecretKey,
}
}

for key, tmpl := range s.Key.Spec.SecretDataFormat {
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)
}

var output bytes.Buffer
if err := goTmpl.Execute(&output, tmplData); err != nil {
return nil, fmt.Errorf("unable to generate secret; failed to exec template in secret data format for key %s: %w", key, err)
}

secretStringData[key] = output.String()
}

secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Expand Down Expand Up @@ -192,35 +180,3 @@ func (s *ObjectStorageKeyScope) ShouldRotateKey() bool {
return s.Key.Status.LastKeyGeneration != nil &&
s.Key.Spec.KeyGeneration != *s.Key.Status.LastKeyGeneration
}

func (s *ObjectStorageKeyScope) ShouldReconcileKeySecret(ctx context.Context) (bool, error) {
if s.Key.Status.SecretName == nil {
return false, nil
}

secret := &corev1.Secret{}
key := client.ObjectKey{Namespace: s.Key.Namespace, Name: *s.Key.Status.SecretName}
err := s.Client.Get(ctx, key, secret)
if apierrors.IsNotFound(err) {
return true, nil
}
if err != nil {
return false, err
}

// Identify an expected key in secret.Data for the desired secret type.
// If it is missing, we must recreate the secret since the secret.type field is immutable.
expectedKey, ok := secretTypeExpectedKey[s.Key.Spec.SecretType]
if !ok {
return false, errors.New("unsupported secret type configured in LinodeObjectStorageKey")
}
if _, ok := secret.Data[expectedKey]; !ok {
if err := s.Client.Delete(ctx, secret); err != nil {
return false, err
}

return true, nil
}

return false, nil
}
Loading

0 comments on commit f9e802e

Please sign in to comment.