Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mutating webhook for defaulting storageclass params #3055

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ spec:
volumeMounts:
- mountPath: /etc/private-key
name: onboarding-private-key
- mountPath: /etc/tls/private
name: webhook-cert-secret
terminationGracePeriodSeconds: 10
securityContext:
runAsNonRoot: true
Expand All @@ -59,3 +61,6 @@ spec:
# this is marked as optional as the secret gets created only in provider mode
optional: true
secretName: onboarding-private-key
- name: webhook-cert-secret
secret:
secretName: ocs-operator-webhook-cert-secret
6 changes: 5 additions & 1 deletion controllers/defaults/defaults.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package defaults contains the default values for various configurable
// options of a StorageCluster
// options of a StorageCluster and constants on StorageConsumer
package defaults

const (
Expand All @@ -24,6 +24,10 @@ const (
// with a value of "false" to disable key rotation. When present, this annotation is then
// propagated to the associated StorageClasses.
KeyRotationEnableAnnotation = "keyrotation.csiaddons.openshift.io/enable"
// Identifies storageconsumer is serving local or remote remote client
StorageConsumerTypeAnnotation = "ocs.openshift.io/storageconsumer-type"
StorageConsumerTypeLocal = "local"
StorageConsumerTypeRemote = "remote"
)

var (
Expand Down
1 change: 1 addition & 0 deletions controllers/storagecluster/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ func (r *StorageClusterReconciler) reconcilePhases(
// list of default ensure functions
// preserve list order
objs = []resourceManager{
&storageClassWebhook{},
&ocsProviderServer{},
&storageClient{},
&backingStorageClasses{},
Expand Down
27 changes: 26 additions & 1 deletion controllers/storagecluster/storagecluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (
ocsv1 "github.com/red-hat-storage/ocs-operator/api/v4/v1"
ocsv1alpha1 "github.com/red-hat-storage/ocs-operator/api/v4/v1alpha1"
"github.com/red-hat-storage/ocs-operator/v4/controllers/util"
"github.com/red-hat-storage/ocs-operator/v4/controllers/webhook"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
admrv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
storagev1 "k8s.io/api/storage/v1"
Expand All @@ -32,6 +34,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
ctrlwebhook "sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

var (
Expand Down Expand Up @@ -211,6 +215,14 @@ func (r *StorageClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
},
}

ocsMutatingWebhookPredicate := builder.WithPredicates(
predicate.NewPredicateFuncs(
func(obj client.Object) bool {
return obj.GetName() == OcsMutatingWebhookConfigName
},
),
)

build := ctrl.NewControllerManagedBy(mgr).
For(&ocsv1.StorageCluster{}, builder.WithPredicates(scPredicate)).
Owns(&cephv1.CephCluster{}, builder.WithPredicates(cephClusterIgnoreTimeUpdatePredicate)).
Expand Down Expand Up @@ -260,7 +272,8 @@ func (r *StorageClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
util.NamePredicate(OdfInfoConfigMapName),
util.NamespacePredicate(r.OperatorNamespace),
),
)
).
Watches(&admrv1.MutatingWebhookConfiguration{}, enqueueStorageClusterRequest, ocsMutatingWebhookPredicate)

if os.Getenv("SKIP_NOOBAA_CRD_WATCH") != "true" {
build.Owns(&nbv1.NooBaa{}, builder.WithPredicates(noobaaIgnoreTimeUpdatePredicate))
Expand All @@ -269,5 +282,17 @@ func (r *StorageClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
build.Watches(&ocsclientv1a1.StorageClient{}, enqueueStorageClusterRequest)
}

mgr.GetWebhookServer().Register(
mutateStorageClassEndpoint,
&ctrlwebhook.Admission{
Handler: &webhook.StorageClassAdmission{
Client: mgr.GetClient(),
Namespace: r.OperatorNamespace,
Decoder: admission.NewDecoder(mgr.GetScheme()),
Log: mgr.GetLogger().WithName("webhook.storageclass"),
},
},
)

return build.Complete(r)
}
1 change: 1 addition & 0 deletions controllers/storagecluster/uninstall_reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ func (r *StorageClusterReconciler) deleteResources(sc *ocsv1.StorageCluster) (re
objs := []resourceManager{
&ocsExternalResources{},
&ocsNoobaaSystem{},
&storageClassWebhook{},
&storageClient{},
&ocsProviderServer{},
&ocsCephRGWRoutes{},
Expand Down
142 changes: 142 additions & 0 deletions controllers/storagecluster/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package storagecluster

import (
"fmt"

ocsv1 "github.com/red-hat-storage/ocs-operator/api/v4/v1"
"github.com/red-hat-storage/ocs-operator/v4/controllers/util"

admrv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

const (
OcsMutatingWebhookConfigName = "ocs-operator.ocs.openshift.io"
WebhookServiceTargetPort = 7443
webhookServicePort = 443
storageClassWebhookName = "storageclass.ocs.openshift.io"
mutateStorageClassEndpoint = "/mutate-storageclass"
// should be the name from rbac/webhook-service.yaml
webhookServiceName = "ocs-operator-webhook-server"
)

type storageClassWebhook struct{}

var _ resourceManager = &storageClassWebhook{}

// should match the spec at rbac/webhook-service.yaml
var webhookService = corev1.Service{
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: "ocs-operator-webhook",
Port: webhookServicePort,
Protocol: corev1.ProtocolTCP,
TargetPort: intstr.FromInt32(WebhookServiceTargetPort),
},
},
Selector: map[string]string{
"name": "ocs-operator",
},
Type: corev1.ServiceTypeClusterIP,
},
}

var storageClassMutatingWebhook = admrv1.MutatingWebhook{
Comment on lines +33 to +50
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need service and webhook created as part of the code not part of yaml files in the CSV? can you please specify the reason and pros and cons?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

svc:
I want to ensure the annotation even if it's removed by mistake and having the svc in bundle doesn't ensure it

webhook:
I need support for setting MatchConditions which aren't exposed on CSV, need typed structs + enforcement and so not part of bundle

ClientConfig: admrv1.WebhookClientConfig{
Service: &admrv1.ServiceReference{
Name: webhookServiceName,
Path: ptr.To(mutateStorageClassEndpoint),
Port: ptr.To(int32(webhookServicePort)),
},
},
Rules: []admrv1.RuleWithOperations{
{
Rule: admrv1.Rule{
APIGroups: []string{"storage.k8s.io"},
APIVersions: []string{"v1"},
Resources: []string{"storageclasses"},
Scope: ptr.To(admrv1.ClusterScope),
},
Operations: []admrv1.OperationType{admrv1.Create},
},
},
FailurePolicy: ptr.To(admrv1.Fail),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wont having FailurePolicy will have sideeffect on other classes if the webhook server is down? can you please check on confirm on it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it'll but while setting webhook I'm using match conditions on our provisioners only, if you are referring non-odf sc as other they are exempted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what will be the impact on the SC for external mode?

SideEffects: ptr.To(admrv1.SideEffectClassNone),
TimeoutSeconds: ptr.To(int32(30)),
AdmissionReviewVersions: []string{"v1"},
MatchConditions: []admrv1.MatchCondition{
{
Name: "onlyBlockAndFileProvisioners",
Expression: fmt.Sprintf(
"request.object.provisioner in ['%s', '%s']",
util.RbdDriverName,
util.CephFSDriverName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nfs need to be added here?

Copy link
Contributor Author

@leelavg leelavg Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a separate PR should follow for NFS, I can't send PR for something I didn't try out.

),
},
},
}

func (s *storageClassWebhook) ensureCreated(r *StorageClusterReconciler, storageCluster *ocsv1.StorageCluster) (ctrl.Result, error) {
svc := &corev1.Service{}
svc.Name = storageClassWebhookName
svc.Namespace = r.OperatorNamespace
if _, err := controllerutil.CreateOrUpdate(r.ctx, r.Client, svc, func() error {
if err := controllerutil.SetControllerReference(storageCluster, svc, r.Scheme); err != nil {
r.Log.Error(err, "failed to own resources", "owner", storageCluster.Name, "dependent", svc.Name)
return err
}
util.AddAnnotation(svc, "service.beta.openshift.io/serving-cert-secret-name", "ocs-operator-webhook-cert-secret")
webhookService.Spec.DeepCopyInto(&svc.Spec)
return nil
}); err != nil {
return ctrl.Result{}, err
}

whConfig := &admrv1.MutatingWebhookConfiguration{}
whConfig.Name = OcsMutatingWebhookConfigName
if _, err := controllerutil.CreateOrUpdate(r.ctx, r.Client, whConfig, func() error {
// openshift fills in the ca on finding this annotation
whConfig.Annotations = map[string]string{
"service.beta.openshift.io/inject-cabundle": "true",
}

var caBundle []byte
if len(whConfig.Webhooks) == 0 {
whConfig.Webhooks = make([]admrv1.MutatingWebhook, 1)
} else {
// do not mutate CA bundle that was injected by openshift
caBundle = whConfig.Webhooks[0].ClientConfig.CABundle
}

// webhook desired state
var wh *admrv1.MutatingWebhook = &whConfig.Webhooks[0]
storageClassMutatingWebhook.DeepCopyInto(wh)
wh.Name = storageClassWebhookName
// preserve the existing (injected) CA bundle if any
wh.ClientConfig.CABundle = caBundle
// send request to the service running in own namespace
wh.ClientConfig.Service.Namespace = r.OperatorNamespace

return nil
}); err != nil {
return ctrl.Result{}, err
}

return ctrl.Result{}, nil
}

func (s *storageClassWebhook) ensureDeleted(r *StorageClusterReconciler, storageCluster *ocsv1.StorageCluster) (ctrl.Result, error) {
whConfig := &admrv1.MutatingWebhookConfiguration{}
whConfig.Name = OcsMutatingWebhookConfigName
if err := r.Delete(r.ctx, whConfig); client.IgnoreNotFound(err) != nil {
r.Log.Error(err, "failed to delete mutating webhook configuration", "name", whConfig.Name)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
129 changes: 129 additions & 0 deletions controllers/webhook/storageclass.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package webhook

import (
"context"
"fmt"
"net/http"
"slices"

"github.com/red-hat-storage/ocs-operator/api/v4/v1alpha1"

"github.com/go-logr/logr"
"github.com/red-hat-storage/ocs-operator/v4/controllers/defaults"
"github.com/red-hat-storage/ocs-operator/v4/controllers/util"
jsonpatch "gomodules.xyz/jsonpatch/v2"
storagev1 "k8s.io/api/storage/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

type StorageClassAdmission struct {
client.Client
Namespace string
Decoder admission.Decoder
Log logr.Logger
}

var supportedProvisioners = []string{
util.RbdDriverName,
util.CephFSDriverName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nfs is missing if its enabled.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a separate PR should follow for NFS, I can't send PR for something I didn't try out.

}

func (s *StorageClassAdmission) Handle(ctx context.Context, req admission.Request) admission.Response {
s.Log.Info("Request received for admission review")

storageClass := &storagev1.StorageClass{}
if err := s.Decoder.Decode(req, storageClass); err != nil {
s.Log.Error(err, "failed to decode admission review as storageclass")
return admission.Errored(http.StatusBadRequest, fmt.Errorf("only storageclasses admission reviews are supported: %v", err))
}

if !slices.Contains(supportedProvisioners, storageClass.Provisioner) {
s.Log.Error(fmt.Errorf("unsupported provisioner %s", storageClass.Provisioner), "failed validation", "storageClass", storageClass.Name)
return admission.Errored(http.StatusBadRequest, fmt.Errorf("supported provisioners are %s", supportedProvisioners))
}

storageConsumerList := &v1alpha1.StorageConsumerList{}
if err := s.List(ctx, storageConsumerList, client.InNamespace(s.Namespace)); err != nil {
s.Log.Error(err, "failed to list storageconsumers", "namespace", s.Namespace)
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("failed to list storageconsumers in %s namespace", s.Namespace))
}

if len(storageConsumerList.Items) == 0 {
s.Log.Error(fmt.Errorf("no storageconsumers found in %s namespace", s.Namespace), "failed validation")
return admission.Denied("creation of storageclass should happen after storageconsumer exists")
}

clusterID := ""
for idx := range storageConsumerList.Items {
consumer := &storageConsumerList.Items[idx]
if consumer.Annotations[defaults.StorageConsumerTypeAnnotation] == defaults.StorageConsumerTypeLocal {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if i create a storageconsumer and add local annotation. no one will block me from doing that isnt it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, either this or an immutable field in the spec, we went w/ the annotation as not to backport spec changes.

let's discuss in upcoming meetings if any change is required.

clusterID = string(consumer.UID)
break
}
}

if clusterID == "" {
s.Log.Error(fmt.Errorf("no storageconsumer is marked as local in %s namespace", s.Namespace), "failed validation")
return admission.Errored(http.StatusInternalServerError, fmt.Errorf("failed to get local storageconsumer in %s namespace", s.Namespace))
}

s.Log.Info("populating default parameters", "storageclass", storageClass.Name, "provisioner", storageClass.Provisioner)
var provisionerSecretName, nodeStageSecretName string
if storageClass.Provisioner == util.RbdDriverName {
provisionerSecretName = getSecretName("rbd", "provisioner", clusterID)
nodeStageSecretName = getSecretName("rbd", "node", clusterID)
} else if storageClass.Provisioner == util.CephFSDriverName {
provisionerSecretName = getSecretName("cephfs", "provisioner", clusterID)
nodeStageSecretName = getSecretName("cephfs", "node", clusterID)
}

patches := []jsonpatch.JsonPatchOperation{}
if len(storageClass.Parameters) != 0 {
patches = append(patches, jsonpatch.JsonPatchOperation{
Operation: "add",
Path: "/parameters",
Value: map[string]string{},
})
s.Log.Info("adding storageclass parameters section")
}
if storageClass.Parameters["csi.storage.k8s.io/provisioner-secret-name"] != "" {
patches = append(patches, jsonpatch.JsonPatchOperation{
Operation: "add",
// forward slash (/) in json key should be replaced with (~1) as per RFC6901
Path: "/parameters/csi.storage.k8s.io~1provisioner-secret-name",
Value: provisionerSecretName,
})
s.Log.Info("populating provisioner secret name in storageclass parameters section")
}
if storageClass.Parameters["csi.storage.k8s.io/node-stage-secret-name"] != "" {
patches = append(patches, jsonpatch.JsonPatchOperation{
Operation: "add",
Path: "/parameters/csi.storage.k8s.io~1node-stage-secret-name",
Value: nodeStageSecretName,
})
s.Log.Info("populating node stage secret name in storageclass parameters section")
}
if storageClass.Parameters["csi.storage.k8s.io/controller-expand-secret-name"] != "" {
patches = append(patches, jsonpatch.JsonPatchOperation{
Operation: "add",
Path: "/parameters/csi.storage.k8s.io~1controller-expand-secret-name",
Value: provisionerSecretName,
})
s.Log.Info("populating controller expand secret name in storageclass parameters section")
}
if storageClass.Parameters["clusterID"] != "" {
patches = append(patches, jsonpatch.JsonPatchOperation{
Operation: "add",
Path: "/parameters/clusterID",
Value: clusterID,
})
s.Log.Info("populating cluster id in storageclass parameters section")
}

return admission.Patched("setting default storageclass parameters if doesn't exist", patches...)
}

func getSecretName(storage, user, id string) string {
return fmt.Sprintf("%s-%s-%s", storage, user, id)
}
Loading
Loading