Skip to content

Commit ccdda60

Browse files
authored
[feat]: linodeobjectstoragebucket: add validating admission webhook on 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
1 parent 843fc27 commit ccdda60

10 files changed

+277
-3
lines changed

PROJECT

+3
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,7 @@ resources:
6969
kind: LinodeObjectStorageBucket
7070
path: github.com/linode/cluster-api-provider-linode/api/v1alpha1
7171
version: v1alpha1
72+
webhooks:
73+
validation: true
74+
webhookVersion: v1
7275
version: "3"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
Copyright 2023 Akamai Technologies, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"context"
21+
"slices"
22+
23+
apierrors "k8s.io/apimachinery/pkg/api/errors"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
"k8s.io/apimachinery/pkg/util/validation/field"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
logf "sigs.k8s.io/controller-runtime/pkg/log"
29+
"sigs.k8s.io/controller-runtime/pkg/webhook"
30+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
31+
32+
. "github.com/linode/cluster-api-provider-linode/clients"
33+
)
34+
35+
var (
36+
// The capability string indicating a region supports Object Storage: [Object Storage Availability]
37+
//
38+
// [Object Storage Availability]: https://www.linode.com/docs/products/storage/object-storage/#availability
39+
LinodeObjectStorageCapability = "Object Storage"
40+
)
41+
42+
// log is for logging in this package.
43+
var linodeobjectstoragebucketlog = logf.Log.WithName("linodeobjectstoragebucket-resource")
44+
45+
// SetupWebhookWithManager will setup the manager to manage the webhooks
46+
func (r *LinodeObjectStorageBucket) SetupWebhookWithManager(mgr ctrl.Manager) error {
47+
return ctrl.NewWebhookManagedBy(mgr).
48+
For(r).
49+
Complete()
50+
}
51+
52+
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable updation and deletion validation.
53+
//+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
54+
55+
var _ webhook.Validator = &LinodeObjectStorageBucket{}
56+
57+
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
58+
func (r *LinodeObjectStorageBucket) ValidateCreate() (admission.Warnings, error) {
59+
linodeobjectstoragebucketlog.Info("validate create", "name", r.Name)
60+
61+
ctx, cancel := context.WithTimeout(context.Background(), defaultWebhookTimeout)
62+
defer cancel()
63+
64+
return nil, r.validateLinodeObjectStorageBucket(ctx, &defaultLinodeClient)
65+
}
66+
67+
// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
68+
func (r *LinodeObjectStorageBucket) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
69+
linodeobjectstoragebucketlog.Info("validate update", "name", r.Name)
70+
71+
// TODO(user): fill in your validation logic upon object update.
72+
return nil, nil
73+
}
74+
75+
// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
76+
func (r *LinodeObjectStorageBucket) ValidateDelete() (admission.Warnings, error) {
77+
linodeobjectstoragebucketlog.Info("validate delete", "name", r.Name)
78+
79+
// TODO(user): fill in your validation logic upon object deletion.
80+
return nil, nil
81+
}
82+
83+
func (r *LinodeObjectStorageBucket) validateLinodeObjectStorageBucket(ctx context.Context, client LinodeClient) error {
84+
var errs field.ErrorList
85+
86+
if err := r.validateLinodeObjectStorageBucketSpec(ctx, client); err != nil {
87+
errs = slices.Concat(errs, err)
88+
}
89+
90+
if len(errs) == 0 {
91+
return nil
92+
}
93+
return apierrors.NewInvalid(
94+
schema.GroupKind{Group: "infrastructure.cluster.x-k8s.io", Kind: "LinodeObjectStorageBucket"},
95+
r.Name, errs)
96+
}
97+
98+
func (r *LinodeObjectStorageBucket) validateLinodeObjectStorageBucketSpec(ctx context.Context, client LinodeClient) field.ErrorList {
99+
var errs field.ErrorList
100+
101+
if err := validateObjectStorageCluster(ctx, client, r.Spec.Cluster, field.NewPath("spec").Child("cluster")); err != nil {
102+
errs = append(errs, err)
103+
}
104+
105+
if len(errs) == 0 {
106+
return nil
107+
}
108+
return errs
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
Copyright 2023 Akamai Technologies, Inc.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"context"
21+
"slices"
22+
"testing"
23+
24+
"github.com/linode/linodego"
25+
"github.com/stretchr/testify/assert"
26+
"go.uber.org/mock/gomock"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
29+
"github.com/linode/cluster-api-provider-linode/mock"
30+
31+
. "github.com/linode/cluster-api-provider-linode/mock/mocktest"
32+
)
33+
34+
func TestValidateLinodeObjectStorageBucket(t *testing.T) {
35+
t.Parallel()
36+
37+
var (
38+
bucket = LinodeObjectStorageBucket{
39+
ObjectMeta: metav1.ObjectMeta{
40+
Name: "example",
41+
Namespace: "example",
42+
},
43+
Spec: LinodeObjectStorageBucketSpec{
44+
Cluster: "example-1",
45+
},
46+
}
47+
region = linodego.Region{ID: "test"}
48+
capabilities = []string{LinodeObjectStorageCapability}
49+
capabilities_zero = []string{}
50+
)
51+
52+
NewSuite(t, mock.MockLinodeClient{}).Run(
53+
OneOf(
54+
Path(
55+
Call("valid", func(ctx context.Context, mck Mock) {
56+
region := region
57+
region.Capabilities = slices.Clone(capabilities)
58+
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(&region, nil).AnyTimes()
59+
}),
60+
Result("success", func(ctx context.Context, mck Mock) {
61+
assert.NoError(t, bucket.validateLinodeObjectStorageBucket(ctx, mck.LinodeClient))
62+
}),
63+
),
64+
),
65+
OneOf(
66+
Path(
67+
Call("invalid cluster format", func(ctx context.Context, mck Mock) {
68+
region := region
69+
region.Capabilities = slices.Clone(capabilities)
70+
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(&region, nil).AnyTimes()
71+
}),
72+
Result("error", func(ctx context.Context, mck Mock) {
73+
bucket := bucket
74+
bucket.Spec.Cluster = "invalid"
75+
assert.Error(t, bucket.validateLinodeObjectStorageBucket(ctx, mck.LinodeClient))
76+
}),
77+
),
78+
Path(
79+
Call("region not supported", func(ctx context.Context, mck Mock) {
80+
region := region
81+
region.Capabilities = slices.Clone(capabilities_zero)
82+
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(&region, nil).AnyTimes()
83+
}),
84+
Result("error", func(ctx context.Context, mck Mock) {
85+
assert.Error(t, bucket.validateLinodeObjectStorageBucket(ctx, mck.LinodeClient))
86+
}),
87+
),
88+
),
89+
)
90+
}

api/v1alpha1/webhook_helpers.go

+22
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"regexp"
78
"slices"
89
"time"
910

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

4950
return plan, nil
5051
}
52+
53+
// validateObjectStorageCluster validates an Object Storage deployment's cluster ID via the following rules:
54+
// - The cluster ID is in the form: REGION_ID-ORDINAL.
55+
// - The region has Object Storage support.
56+
//
57+
// NOTE: This implementation intended to bypass the authentication requirement for the [Clusters List] and [Cluster
58+
// View] endpoints in the Linode API, thereby reusing a [github.com/linode/linodego.Client] (and its caching if enabled)
59+
// across many admission requests.
60+
//
61+
// [Clusters List]: https://www.linode.com/docs/api/object-storage/#clusters-list
62+
// [Cluster View]: https://www.linode.com/docs/api/object-storage/#cluster-view
63+
func validateObjectStorageCluster(ctx context.Context, client LinodeClient, id string, path *field.Path) *field.Error {
64+
//nolint:gocritic // prefer no escapes
65+
cexp := regexp.MustCompile("^(([[:lower:]]+-)*[[:lower:]]+)-[[:digit:]]+$")
66+
if !cexp.MatchString(id) {
67+
return field.Invalid(path, id, "must be in form: region_id-ordinal")
68+
}
69+
70+
region := cexp.FindStringSubmatch(id)[1]
71+
return validateRegion(ctx, client, region, path, LinodeObjectStorageCapability)
72+
}

api/v1alpha1/webhook_suite_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ var _ = BeforeSuite(func() {
123123
err = (&LinodeVPC{}).SetupWebhookWithManager(mgr)
124124
Expect(err).NotTo(HaveOccurred())
125125

126+
err = (&LinodeObjectStorageBucket{}).SetupWebhookWithManager(mgr)
127+
Expect(err).NotTo(HaveOccurred())
128+
126129
//+kubebuilder:scaffold:webhook
127130

128131
go func() {

cmd/main.go

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func init() {
5454
// +kubebuilder:scaffold:scheme
5555
}
5656

57+
//nolint:cyclop // main
5758
func main() {
5859
var (
5960
// Environment variables
@@ -162,6 +163,10 @@ func main() {
162163
setupLog.Error(err, "unable to create webhook", "webhook", "LinodeVPC")
163164
os.Exit(1)
164165
}
166+
if err = (&infrastructurev1alpha1.LinodeObjectStorageBucket{}).SetupWebhookWithManager(mgr); err != nil {
167+
setupLog.Error(err, "unable to create webhook", "webhook", "LinodeObjectStorageBucket")
168+
os.Exit(1)
169+
}
165170
}
166171
// +kubebuilder:scaffold:builder
167172

config/crd/kustomization.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ resources:
2121
patches:
2222
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
2323
# patches here are for enabling the conversion webhook for each CRD
24-
#- path: patches/webhook_in_linodeclusters.yaml
24+
- path: patches/webhook_in_linodeclusters.yaml
2525
- path: patches/webhook_in_linodemachines.yaml
2626
#- path: patches/webhook_in_linodemachinetemplates.yaml
2727
#- path: patches/webhook_in_linodeclustertemplates.yaml
2828
- path: patches/webhook_in_linodevpcs.yaml
29-
#- path: patches/webhook_in_linodeobjectstoragebuckets.yaml
29+
- path: patches/webhook_in_linodeobjectstoragebuckets.yaml
3030
#+kubebuilder:scaffold:crdkustomizewebhookpatch
3131

3232
# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
@@ -36,7 +36,7 @@ patches:
3636
#- path: patches/cainjection_in_linodemachinetemplates.yaml
3737
#- path: patches/cainjection_in_linodeclustertemplates.yaml
3838
- path: patches/cainjection_in_linodevpcs.yaml
39-
#- path: patches/cainjection_in_linodeobjectstoragebuckets.yaml
39+
- path: patches/cainjection_in_linodeobjectstoragebuckets.yaml
4040
#+kubebuilder:scaffold:crdkustomizecainjectionpatch
4141

4242
# [VALIDATION]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# The following patch adds a directive for certmanager to inject CA into the CRD
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
annotations:
6+
cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME
7+
name: linodeobjectstoragebuckets.infrastructure.cluster.x-k8s.io
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# The following patch enables a conversion webhook for the CRD
2+
apiVersion: apiextensions.k8s.io/v1
3+
kind: CustomResourceDefinition
4+
metadata:
5+
name: linodeobjectstoragebuckets.infrastructure.cluster.x-k8s.io
6+
spec:
7+
conversion:
8+
strategy: Webhook
9+
webhook:
10+
clientConfig:
11+
service:
12+
namespace: system
13+
name: webhook-service
14+
path: /convert
15+
conversionReviewVersions:
16+
- v1

config/webhook/manifests.yaml

+19
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ webhooks:
4242
resources:
4343
- linodemachines
4444
sideEffects: None
45+
- admissionReviewVersions:
46+
- v1
47+
clientConfig:
48+
service:
49+
name: webhook-service
50+
namespace: system
51+
path: /validate-infrastructure-cluster-x-k8s-io-v1alpha1-linodeobjectstoragebucket
52+
failurePolicy: Fail
53+
name: vlinodeobjectstoragebucket.kb.io
54+
rules:
55+
- apiGroups:
56+
- infrastructure.cluster.x-k8s.io
57+
apiVersions:
58+
- v1alpha1
59+
operations:
60+
- CREATE
61+
resources:
62+
- linodeobjectstoragebuckets
63+
sideEffects: None
4564
- admissionReviewVersions:
4665
- v1
4766
clientConfig:

0 commit comments

Comments
 (0)