Skip to content

Commit

Permalink
[feat]: linodeobjectstoragebucket: add validating admission webhook o…
Browse files Browse the repository at this point in the history
…n create (#330)

* feat: linodeobjectstoragebucket: kubebuilder create webhook

Scaffold a validating admission webhook for the LinodeObjectStorageBucket
resource with Kubebuider via the command:
kubebuilder create webhook --group infrastructure --version v1alpha1 --kind LinodeObjectStorageBucket --programmatic-validation

* fixup! feat: linodeobjectstoragebucket: kubebuilder create webhook

* api: linodeobjectstoragebucket: add create validation
  • Loading branch information
cbzzz authored May 23, 2024
1 parent 843fc27 commit ccdda60
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 3 deletions.
3 changes: 3 additions & 0 deletions PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,7 @@ resources:
kind: LinodeObjectStorageBucket
path: github.com/linode/cluster-api-provider-linode/api/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
version: "3"
109 changes: 109 additions & 0 deletions api/v1alpha1/linodeobjectstoragebucket_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
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 v1alpha1

import (
"context"
"slices"

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"
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"

. "github.com/linode/cluster-api-provider-linode/clients"
)

var (
// The capability string indicating a region supports Object Storage: [Object Storage Availability]
//
// [Object Storage Availability]: https://www.linode.com/docs/products/storage/object-storage/#availability
LinodeObjectStorageCapability = "Object Storage"
)

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

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *LinodeObjectStorageBucket) 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 updation and deletion validation.
//+kubebuilder:webhook:path=/validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodeobjectstoragebucket,mutating=false,failurePolicy=fail,sideEffects=None,groups=infrastructure.cluster.x-k8s.io,resources=linodeobjectstoragebuckets,verbs=create,versions=v1alpha1,name=vlinodeobjectstoragebucket.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &LinodeObjectStorageBucket{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *LinodeObjectStorageBucket) ValidateCreate() (admission.Warnings, error) {
linodeobjectstoragebucketlog.Info("validate create", "name", r.Name)

ctx, cancel := context.WithTimeout(context.Background(), defaultWebhookTimeout)
defer cancel()

return nil, r.validateLinodeObjectStorageBucket(ctx, &defaultLinodeClient)
}

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

// TODO(user): fill in your validation logic upon object update.
return nil, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *LinodeObjectStorageBucket) ValidateDelete() (admission.Warnings, error) {
linodeobjectstoragebucketlog.Info("validate delete", "name", r.Name)

// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}

func (r *LinodeObjectStorageBucket) validateLinodeObjectStorageBucket(ctx context.Context, client LinodeClient) error {
var errs field.ErrorList

if err := r.validateLinodeObjectStorageBucketSpec(ctx, client); err != nil {
errs = slices.Concat(errs, err)
}

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

func (r *LinodeObjectStorageBucket) validateLinodeObjectStorageBucketSpec(ctx context.Context, client LinodeClient) field.ErrorList {
var errs field.ErrorList

if err := validateObjectStorageCluster(ctx, client, r.Spec.Cluster, field.NewPath("spec").Child("cluster")); err != nil {
errs = append(errs, err)
}

if len(errs) == 0 {
return nil
}
return errs
}
90 changes: 90 additions & 0 deletions api/v1alpha1/linodeobjectstoragebucket_webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
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 v1alpha1

import (
"context"
"slices"
"testing"

"github.com/linode/linodego"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/linode/cluster-api-provider-linode/mock"

. "github.com/linode/cluster-api-provider-linode/mock/mocktest"
)

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

var (
bucket = LinodeObjectStorageBucket{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "example",
},
Spec: LinodeObjectStorageBucketSpec{
Cluster: "example-1",
},
}
region = linodego.Region{ID: "test"}
capabilities = []string{LinodeObjectStorageCapability}
capabilities_zero = []string{}
)

NewSuite(t, mock.MockLinodeClient{}).Run(
OneOf(
Path(
Call("valid", func(ctx context.Context, mck Mock) {
region := region
region.Capabilities = slices.Clone(capabilities)
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(&region, nil).AnyTimes()
}),
Result("success", func(ctx context.Context, mck Mock) {
assert.NoError(t, bucket.validateLinodeObjectStorageBucket(ctx, mck.LinodeClient))
}),
),
),
OneOf(
Path(
Call("invalid cluster format", func(ctx context.Context, mck Mock) {
region := region
region.Capabilities = slices.Clone(capabilities)
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(&region, nil).AnyTimes()
}),
Result("error", func(ctx context.Context, mck Mock) {
bucket := bucket
bucket.Spec.Cluster = "invalid"
assert.Error(t, bucket.validateLinodeObjectStorageBucket(ctx, mck.LinodeClient))
}),
),
Path(
Call("region not supported", func(ctx context.Context, mck Mock) {
region := region
region.Capabilities = slices.Clone(capabilities_zero)
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(&region, nil).AnyTimes()
}),
Result("error", func(ctx context.Context, mck Mock) {
assert.Error(t, bucket.validateLinodeObjectStorageBucket(ctx, mck.LinodeClient))
}),
),
),
)
}
22 changes: 22 additions & 0 deletions api/v1alpha1/webhook_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
"regexp"
"slices"
"time"

Expand Down Expand Up @@ -48,3 +49,24 @@ func validateLinodeType(ctx context.Context, client LinodeClient, id string, pat

return plan, nil
}

// validateObjectStorageCluster validates an Object Storage deployment's cluster ID via the following rules:
// - The cluster ID is in the form: REGION_ID-ORDINAL.
// - The region has Object Storage support.
//
// NOTE: This implementation intended to bypass the authentication requirement for the [Clusters List] and [Cluster
// View] endpoints in the Linode API, thereby reusing a [github.com/linode/linodego.Client] (and its caching if enabled)
// across many admission requests.
//
// [Clusters List]: https://www.linode.com/docs/api/object-storage/#clusters-list
// [Cluster View]: https://www.linode.com/docs/api/object-storage/#cluster-view
func validateObjectStorageCluster(ctx context.Context, client LinodeClient, id string, path *field.Path) *field.Error {
//nolint:gocritic // prefer no escapes
cexp := regexp.MustCompile("^(([[:lower:]]+-)*[[:lower:]]+)-[[:digit:]]+$")
if !cexp.MatchString(id) {
return field.Invalid(path, id, "must be in form: region_id-ordinal")
}

region := cexp.FindStringSubmatch(id)[1]
return validateRegion(ctx, client, region, path, LinodeObjectStorageCapability)
}
3 changes: 3 additions & 0 deletions api/v1alpha1/webhook_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ var _ = BeforeSuite(func() {
err = (&LinodeVPC{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

err = (&LinodeObjectStorageBucket{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

//+kubebuilder:scaffold:webhook

go func() {
Expand Down
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func init() {
// +kubebuilder:scaffold:scheme
}

//nolint:cyclop // main
func main() {
var (
// Environment variables
Expand Down Expand Up @@ -162,6 +163,10 @@ func main() {
setupLog.Error(err, "unable to create webhook", "webhook", "LinodeVPC")
os.Exit(1)
}
if err = (&infrastructurev1alpha1.LinodeObjectStorageBucket{}).SetupWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "LinodeObjectStorageBucket")
os.Exit(1)
}
}
// +kubebuilder:scaffold:builder

Expand Down
6 changes: 3 additions & 3 deletions config/crd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ resources:
patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
#- path: patches/webhook_in_linodeclusters.yaml
- path: patches/webhook_in_linodeclusters.yaml
- path: patches/webhook_in_linodemachines.yaml
#- path: patches/webhook_in_linodemachinetemplates.yaml
#- path: patches/webhook_in_linodeclustertemplates.yaml
- path: patches/webhook_in_linodevpcs.yaml
#- path: patches/webhook_in_linodeobjectstoragebuckets.yaml
- path: patches/webhook_in_linodeobjectstoragebuckets.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch

# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
Expand All @@ -36,7 +36,7 @@ patches:
#- path: patches/cainjection_in_linodemachinetemplates.yaml
#- path: patches/cainjection_in_linodeclustertemplates.yaml
- path: patches/cainjection_in_linodevpcs.yaml
#- path: patches/cainjection_in_linodeobjectstoragebuckets.yaml
- path: patches/cainjection_in_linodeobjectstoragebuckets.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch

# [VALIDATION]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
name: linodeobjectstoragebuckets.infrastructure.cluster.x-k8s.io
16 changes: 16 additions & 0 deletions config/crd/patches/webhook_in_linodeobjectstoragebuckets.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: linodeobjectstoragebuckets.infrastructure.cluster.x-k8s.io
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1
19 changes: 19 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ webhooks:
resources:
- linodemachines
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodeobjectstoragebucket
failurePolicy: Fail
name: vlinodeobjectstoragebucket.kb.io
rules:
- apiGroups:
- infrastructure.cluster.x-k8s.io
apiVersions:
- v1alpha1
operations:
- CREATE
resources:
- linodeobjectstoragebuckets
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
Expand Down

0 comments on commit ccdda60

Please sign in to comment.