diff --git a/charts/nebula-operator/templates/_helpers.tpl b/charts/nebula-operator/templates/_helpers.tpl index 5034201c..880d6f54 100644 --- a/charts/nebula-operator/templates/_helpers.tpl +++ b/charts/nebula-operator/templates/_helpers.tpl @@ -97,6 +97,20 @@ Admission webhook name of the chart. {{ include "nebula-operator.name" . }}-webhook {{- end }} +{{/* +Controller Manager Admission webhook name. +*/}} +{{- define "controller-manager-admission-webhook.name" -}} +controller-manager-{{ include "nebula-operator.name" . }}-webhook +{{- end }} + +{{/* +Autoscaler Admission webhook name. +*/}} +{{- define "autoscaler-admission-webhook.name" -}} +autoscaler-{{ include "nebula-operator.name" . }}-webhook +{{- end }} + {{/* Admission webhook selector labels */}} diff --git a/charts/nebula-operator/templates/admission-webhook-certificate.yaml b/charts/nebula-operator/templates/admission-webhook-certificate.yaml index 1151b603..9ab758a6 100644 --- a/charts/nebula-operator/templates/admission-webhook-certificate.yaml +++ b/charts/nebula-operator/templates/admission-webhook-certificate.yaml @@ -1,4 +1,4 @@ -{{- if .Values.admissionWebhook.create }} +{{- if and (or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create) .Values.admissionWebhook.useCertManager }} --- apiVersion: cert-manager.io/v1 kind: Certificate @@ -9,8 +9,10 @@ metadata: {{- include "admission-webhook.labels" . | nindent 4 }} spec: dnsNames: - - {{ template "admission-webhook.name" . }}-service.{{ template "nebula-operator.namespace" . }}.svc - - {{ template "admission-webhook.name" . }}-service.{{ template "nebula-operator.namespace" . }}.svc.{{ default "cluster.local" .Values.kubernetesClusterDomain }} + - {{ template "controller-manager-admission-webhook.name" . }}-service.{{ template "nebula-operator.namespace" . }}.svc + - {{ template "controller-manager-admission-webhook.name" . }}-service.{{ template "nebula-operator.namespace" . }}.svc.{{ default "cluster.local" .Values.kubernetesClusterDomain }} + - {{ template "autoscaler-admission-webhook.name" . }}-service.{{ template "nebula-operator.namespace" . }}.svc + - {{ template "autoscaler-admission-webhook.name" . }}-service.{{ template "nebula-operator.namespace" . }}.svc.{{ default "cluster.local" .Values.kubernetesClusterDomain }} issuerRef: kind: Issuer name: {{ template "admission-webhook.name" . }}-issuer diff --git a/charts/nebula-operator/templates/admission-webhook-registration.yaml b/charts/nebula-operator/templates/admission-webhook-registration.yaml index d9421714..85bf66d3 100644 --- a/charts/nebula-operator/templates/admission-webhook-registration.yaml +++ b/charts/nebula-operator/templates/admission-webhook-registration.yaml @@ -1,20 +1,23 @@ -{{- if .Values.admissionWebhook.create }} +{{- if or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create }} --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: + {{- if .Values.admissionWebhook.useCertManager }} annotations: cert-manager.io/inject-ca-from: {{ template "nebula-operator.namespace" . }}/{{ template "admission-webhook.name" . }}-cert + {{- end }} name: {{ template "admission-webhook.name" . }}-validating labels: {{- include "admission-webhook.labels" . | nindent 4 }} webhooks: + {{- if .Values.admissionWebhook.contollerManagerAdmissionWebhook.create }} - name: nebulaclustervalidating.nebula-graph.io admissionReviewVersions: - v1 clientConfig: service: - name: {{ template "admission-webhook.name" . }}-service + name: {{ template "controller-manager-admission-webhook.name" . }}-service namespace: {{ template "nebula-operator.namespace" . }} path: /validate-nebulacluster failurePolicy: Fail @@ -32,5 +35,31 @@ webhooks: scope: "*" sideEffects: None timeoutSeconds: 3 + {{- end }} + + {{- if .Values.admissionWebhook.autoscalerAdmissionWebhook.create }} + - name: nebulaautoscalingvalidating.nebula-graph.io + admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ template "autoscaler-admission-webhook.name" . }}-service + namespace: {{ template "nebula-operator.namespace" . }} + path: /validate-nebulaautoscaler + failurePolicy: Fail + rules: + - apiGroups: + - autoscaling.nebula-graph.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - nebulaautoscalers + scope: "*" + sideEffects: None + timeoutSeconds: 3 + {{- end }} {{- end }} diff --git a/charts/nebula-operator/templates/autoscaler-admission-webhook-service.yaml b/charts/nebula-operator/templates/autoscaler-admission-webhook-service.yaml new file mode 100644 index 00000000..dc580b3e --- /dev/null +++ b/charts/nebula-operator/templates/autoscaler-admission-webhook-service.yaml @@ -0,0 +1,17 @@ +{{- if .Values.admissionWebhook.autoscalerAdmissionWebhook.create }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ template "autoscaler-admission-webhook.name" . }}-service + namespace: {{ template "nebula-operator.namespace" . }} + labels: + {{- include "admission-webhook.labels" . | nindent 4 }} +spec: + ports: + - port: 443 + targetPort: 9448 + selector: + {{- include "admission-webhook.matchLabels" . | nindent 4 }} +{{- end }} + diff --git a/charts/nebula-operator/templates/admission-webhook-service.yaml b/charts/nebula-operator/templates/controller-manager-admission-webhook-service.yaml similarity index 67% rename from charts/nebula-operator/templates/admission-webhook-service.yaml rename to charts/nebula-operator/templates/controller-manager-admission-webhook-service.yaml index 861c3bbb..60cfbd24 100644 --- a/charts/nebula-operator/templates/admission-webhook-service.yaml +++ b/charts/nebula-operator/templates/controller-manager-admission-webhook-service.yaml @@ -1,9 +1,9 @@ -{{- if .Values.admissionWebhook.create }} +{{- if .Values.admissionWebhook.contollerManagerAdmissionWebhook.create }} --- apiVersion: v1 kind: Service metadata: - name: {{ template "admission-webhook.name" . }}-service + name: {{ template "controller-manager-admission-webhook.name" . }}-service namespace: {{ template "nebula-operator.namespace" . }} labels: {{- include "admission-webhook.labels" . | nindent 4 }} diff --git a/charts/nebula-operator/templates/controller-manager-deployment.yaml b/charts/nebula-operator/templates/controller-manager-deployment.yaml index 7f7a3aad..600281fd 100644 --- a/charts/nebula-operator/templates/controller-manager-deployment.yaml +++ b/charts/nebula-operator/templates/controller-manager-deployment.yaml @@ -1,4 +1,17 @@ {{- if .Values.controllerManager.create }} +{{- if and (or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create) (not .Values.admissionWebhook.useCertManager) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "admission-webhook.name" . }}-secret + namespace: {{ template "nebula-operator.namespace" . }} +type: kubernetes.io/tls +data: + tls.crt: "" + tls.key: "" + ca.crt: "" +{{- end }} --- apiVersion: apps/v1 kind: Deployment @@ -43,8 +56,15 @@ spec: - --concurrent-nebulabackup-syncs={{ .Values.concurrentNebulaBackupSyncs }} - --leader-elect - --leader-elect-resource-namespace={{ template "nebula-operator.namespace" . }} - - --enable-admission-webhook={{ .Values.admissionWebhook.create }} - - --webhook-secure-port={{ .Values.admissionWebhook.webhookBindPort }} + - --enable-admission-webhook={{ .Values.admissionWebhook.contollerManagerAdmissionWebhook.create }} + - --webhook-secure-port={{ .Values.admissionWebhook.contollerManagerAdmissionWebhook.webhookBindPort }} + - --webhook-namespace={{ template "nebula-operator.namespace" . }} + - --webhook-server-name={{ template "admission-webhook.name" . }}-validating + - --webhook-names={{ template "controller-manager-admission-webhook.name" . }}-service,{{ template "autoscaler-admission-webhook.name" . }}-service + - --certificate-validity={{ .Values.admissionWebhook.certValidity }} + - --secret-namespace={{ template "nebula-operator.namespace" . }} + - --secret-name={{ template "admission-webhook.name" . }}-secret + - --kube-domain={{ default "cluster.local" .Values.kubernetesClusterDomain }} - --enable-kruise-scheme={{ .Values.enableKruiseScheme }} - --v={{ .Values.controllerManager.verbosity }} {{- if or .Values.kubernetesClusterDomain .Values.controllerManager.env }} @@ -55,9 +75,9 @@ spec: {{- end }} {{- if .Values.controllerManager.env }}{{ toYaml .Values.controllerManager.env | nindent 12 }}{{- end }} {{- end }} - {{- if .Values.admissionWebhook.create }} + {{- if .Values.admissionWebhook.contollerManagerAdmissionWebhook.create }} ports: - - containerPort: {{ .Values.admissionWebhook.webhookBindPort | default 9443 }} + - containerPort: {{ .Values.admissionWebhook.contollerManagerAdmissionWebhook.webhookBindPort | default 9443 }} name: webhook-server protocol: TCP {{- end }} @@ -77,9 +97,9 @@ spec: periodSeconds: 10 securityContext: allowPrivilegeEscalation: false - {{- if or .Values.controllerManager.extraVolumeMounts .Values.admissionWebhook.create }} + {{- if or .Values.controllerManager.extraVolumeMounts .Values.admissionWebhook.contollerManagerAdmissionWebhook.create }} volumeMounts: - {{- if .Values.admissionWebhook.create }} + {{- if .Values.admissionWebhook.contollerManagerAdmissionWebhook.create }} - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert readOnly: true @@ -100,6 +120,15 @@ spec: - --leader-elect - --leader-elect-resource-namespace={{ template "nebula-operator.namespace" . }} - --v={{ .Values.controllerManager.verbosity }} + - --enable-admission-webhook={{ .Values.admissionWebhook.autoscalerAdmissionWebhook.create }} + - --webhook-secure-port={{ .Values.admissionWebhook.autoscalerAdmissionWebhook.webhookBindPort }} + - --webhook-namespace={{ template "nebula-operator.namespace" . }} + - --webhook-server-name={{ template "admission-webhook.name" . }}-validating + - --webhook-names={{ template "controller-manager-admission-webhook.name" . }}-service,{{ template "autoscaler-admission-webhook.name" . }}-service + - --certificate-validity={{ .Values.admissionWebhook.certValidity }} + - --secret-namespace={{ template "nebula-operator.namespace" . }} + - --secret-name={{ template "admission-webhook.name" . }}-secret + - --kube-domain={{ default "cluster.local" .Values.kubernetesClusterDomain }} {{- if or .Values.kubernetesClusterDomain .Values.controllerManager.env }} env: {{- if .Values.kubernetesClusterDomain }} @@ -108,6 +137,12 @@ spec: {{- end }} {{- if .Values.controllerManager.env }}{{ toYaml .Values.controllerManager.env | nindent 12 }}{{- end }} {{- end }} + {{- if .Values.admissionWebhook.autoscalerAdmissionWebhook.create }} + ports: + - containerPort: {{ .Values.admissionWebhook.autoscalerAdmissionWebhook.webhookBindPort | default 9448 }} + name: webhook-server + protocol: TCP + {{- end }} resources: {{- toYaml .Values.controllerManager.resources | nindent 12 }} livenessProbe: @@ -124,6 +159,12 @@ spec: periodSeconds: 10 securityContext: allowPrivilegeEscalation: false + {{- if .Values.admissionWebhook.autoscalerAdmissionWebhook.create }} + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + {{- end }} {{- with .Values.controllerManager.sidecarContainers }} {{- range $name, $spec := $.Values.controllerManager.sidecarContainers }} - name: {{ $name }} @@ -153,9 +194,9 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{- if or .Values.controllerManager.extraVolumes .Values.admissionWebhook.create }} + {{- if or .Values.controllerManager.extraVolumes (or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create) }} volumes: - {{- if .Values.admissionWebhook.create }} + {{- if or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create}} - name: cert secret: defaultMode: 420 diff --git a/charts/nebula-operator/templates/controller-manager-rbac.yaml b/charts/nebula-operator/templates/controller-manager-rbac.yaml index a202d6b0..a96d6c8a 100644 --- a/charts/nebula-operator/templates/controller-manager-rbac.yaml +++ b/charts/nebula-operator/templates/controller-manager-rbac.yaml @@ -37,6 +37,19 @@ rules: verbs: - create - patch + {{- if and (or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create) (not .Values.admissionWebhook.useCertManager) }} + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - update + - patch + {{- end}} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding @@ -387,6 +400,18 @@ rules: verbs: - get - list + {{- if and (or .Values.admissionWebhook.contollerManagerAdmissionWebhook.create .Values.admissionWebhook.autoscalerAdmissionWebhook.create) (not .Values.admissionWebhook.useCertManager) }} + - apiGroups: + - admissionregistration.k8s.io + resources: + - validatingwebhookconfigurations + verbs: + - get + - list + - watch + - update + - patch + {{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/charts/nebula-operator/values.yaml b/charts/nebula-operator/values.yaml index 87f2a073..9d70450e 100644 --- a/charts/nebula-operator/values.yaml +++ b/charts/nebula-operator/values.yaml @@ -64,9 +64,17 @@ controllerManager: # runAsNonRoot: true admissionWebhook: - create: false - # The TCP port the Webhook server binds to. (default 9443) - webhookBindPort: 9443 + contollerManagerAdmissionWebhook: + create: false + # The TCP port the Webhook server binds to. (default 9443) + webhookBindPort: 9443 + autoscalerAdmissionWebhook: + create: true + # The TCP port the Webhook server binds to. (default 9448) + webhookBindPort: 9448 + useCertManager: false + # Number of days. Only needed if useCertManager is false. (default 1) + certValidity: 1 scheduler: create: true diff --git a/cmd/autoscaler/app/autoscaler.go b/cmd/autoscaler/app/autoscaler.go index 02693bc7..35cc3be1 100644 --- a/cmd/autoscaler/app/autoscaler.go +++ b/cmd/autoscaler/app/autoscaler.go @@ -19,6 +19,7 @@ package app import ( "context" "flag" + "net/http" "github.com/spf13/cobra" cliflag "k8s.io/component-base/cli/flag" @@ -28,14 +29,18 @@ import ( "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/config" "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/vesoft-inc/nebula-operator/apis/autoscaling/scheme" "github.com/vesoft-inc/nebula-operator/apis/autoscaling/v1alpha1" "github.com/vesoft-inc/nebula-operator/cmd/autoscaler/app/options" + certrot "github.com/vesoft-inc/nebula-operator/pkg/cert-rotation" "github.com/vesoft-inc/nebula-operator/pkg/controller/autoscaler" klogflag "github.com/vesoft-inc/nebula-operator/pkg/flag/klog" profileflag "github.com/vesoft-inc/nebula-operator/pkg/flag/profile" "github.com/vesoft-inc/nebula-operator/pkg/version" + nawebhook "github.com/vesoft-inc/nebula-operator/pkg/webhook/autoscaler" ) // NewAutoscalerCommand creates a *cobra.Command object with default parameters @@ -106,6 +111,17 @@ func Run(ctx context.Context, opts *options.Options) error { }, } + if opts.EnableAdmissionWebhook { + ctrlOptions.WebhookServer = webhook.NewServer(webhook.Options{ + Host: opts.WebhookOpts.BindAddress, + Port: opts.WebhookOpts.SecurePort, + CertDir: opts.WebhookOpts.CertDir, + CertName: opts.WebhookOpts.CertName, + KeyName: opts.WebhookOpts.KeyName, + TLSMinVersion: opts.WebhookOpts.TLSMinVersion, + }) + } + mgr, err := ctrlruntime.NewManager(cfg, ctrlOptions) if err != nil { klog.Errorf("Failed to build nebula-autoscaler: %v", err) @@ -126,6 +142,30 @@ func Run(ctx context.Context, opts *options.Options) error { return err } + if opts.EnableAdmissionWebhook { + decoder := admission.NewDecoder(mgr.GetScheme()) + klog.Info("Registering webhooks to nebula-auto-scaler") + hookServer := mgr.GetWebhookServer() + hookServer.Register("/validate-nebulaautoscaler", + &webhook.Admission{Handler: &nawebhook.ValidatingAdmission{Decoder: decoder}}) + hookServer.WebhookMux().Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{})) + + // Start certificate rotation + certGenerator := certrot.CertGenerator{ + LeaderElection: opts.LeaderElection, + WebhookNames: opts.WebhookOpts.WebhookNames, + WebhookServerName: opts.WebhookOpts.WebhookServerName, + WebhookNamespace: opts.WebhookOpts.WebhookNamespace, + CertDir: opts.WebhookOpts.CertDir, + CertValidity: opts.WebhookOpts.CertValidity, + SecretName: opts.WebhookOpts.SecretName, + SecretNamespace: opts.WebhookOpts.SecretNamespace, + KubernetesDomain: opts.WebhookOpts.KubernetesDomain, + } + + certGenerator.Run(ctx) + } + if err := mgr.AddHealthzCheck("ping", healthz.Ping); err != nil { klog.Errorf("failed to add health check endpoint: %v", err) return err diff --git a/cmd/autoscaler/app/options/options.go b/cmd/autoscaler/app/options/options.go index a92a24fc..77e2788a 100644 --- a/cmd/autoscaler/app/options/options.go +++ b/cmd/autoscaler/app/options/options.go @@ -27,6 +27,7 @@ import ( ctrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1" "github.com/vesoft-inc/nebula-operator/pkg/flag/profile" + "github.com/vesoft-inc/nebula-operator/pkg/flag/webhook" ) const ( @@ -57,6 +58,9 @@ type Options struct { // HPAOpts defines the configuration of autoscaler controller. HPAOpts ctrlmgrconfigv1alpha1.HPAControllerConfiguration + // EnableAdmissionWebhook enable admission webhook for autoscaler. + EnableAdmissionWebhook bool + // MetricsBindAddress is the TCP address that the controller should bind to // for serving prometheus metrics. // It can be set to "0" to disable the metrics serving. @@ -70,6 +74,7 @@ type Options struct { HealthProbeBindAddress string ProfileOpts profile.Options + WebhookOpts webhook.Options } func NewOptions() *Options { @@ -110,7 +115,10 @@ func (o *Options) AddFlags(flags *pflag.FlagSet) { flags.DurationVar(&o.HPAOpts.HorizontalPodAutoscalerInitialReadinessDelay.Duration, "autoscaler-initial-readiness-delay", defaultAutoscalerInitialReadinessDelay.Duration, "The period after pod start during which readiness changes will be treated as initial readiness.") flags.StringVar(&o.MetricsBindAddress, "metrics-bind-address", ":8080", "The TCP address that the controller should bind to for serving prometheus metrics(e.g. 127.0.0.1:8080, :8080). It can be set to \"0\" to disable the metrics serving.") + flags.BoolVar(&o.EnableAdmissionWebhook, "enable-admission-webhook", false, "If set to ture enable admission webhook for autoscaler.") flags.StringVar(&o.HealthProbeBindAddress, "health-probe-bind-address", ":8081", "The TCP address that the controller should bind to for serving health probes.(e.g. 127.0.0.1:8081, :8081). It can be set to \"0\" to disable the health probe serving.") + o.WebhookOpts.AddFlags(flags) + //flags.StringSliceVar(&o.Namespaces, "watch-namespaces", nil, "Namespaces restricts the controller watches for updates to Kubernetes objects. If empty, all namespaces are watched. Multiple namespaces seperated by comma.(e.g. ns1,ns2,ns3).") } diff --git a/cmd/controller-manager/app/controller-manager.go b/cmd/controller-manager/app/controller-manager.go index 10f43f96..d31f359b 100644 --- a/cmd/controller-manager/app/controller-manager.go +++ b/cmd/controller-manager/app/controller-manager.go @@ -40,6 +40,7 @@ import ( "github.com/vesoft-inc/nebula-operator/apis/apps/v1alpha1" "github.com/vesoft-inc/nebula-operator/cmd/controller-manager/app/options" + certrot "github.com/vesoft-inc/nebula-operator/pkg/cert-rotation" "github.com/vesoft-inc/nebula-operator/pkg/controller/cronbackup" "github.com/vesoft-inc/nebula-operator/pkg/controller/nebulabackup" "github.com/vesoft-inc/nebula-operator/pkg/controller/nebulacluster" @@ -206,6 +207,21 @@ func Run(ctx context.Context, opts *options.Options) error { hookServer.Register("/validate-nebulacluster", &webhook.Admission{Handler: &ncwebhook.ValidatingAdmission{Decoder: decoder}}) hookServer.WebhookMux().Handle("/readyz/", http.StripPrefix("/readyz/", &healthz.Handler{})) + + // Start certificate rotation + certGenerator := certrot.CertGenerator{ + LeaderElection: opts.LeaderElection, + WebhookNames: opts.WebhookOpts.WebhookNames, + WebhookServerName: opts.WebhookOpts.WebhookServerName, + WebhookNamespace: opts.WebhookOpts.WebhookNamespace, + CertDir: opts.WebhookOpts.CertDir, + CertValidity: opts.WebhookOpts.CertValidity, + SecretName: opts.WebhookOpts.SecretName, + SecretNamespace: opts.WebhookOpts.SecretNamespace, + KubernetesDomain: opts.WebhookOpts.KubernetesDomain, + } + + certGenerator.Run(ctx) } if err := mgr.AddHealthzCheck("ping", healthz.Ping); err != nil { diff --git a/pkg/cert-rotation/certificate-generator.go b/pkg/cert-rotation/certificate-generator.go new file mode 100644 index 00000000..05d1b95a --- /dev/null +++ b/pkg/cert-rotation/certificate-generator.go @@ -0,0 +1,392 @@ +/* +Copyright 2024 Vesoft 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 app + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "os" + "path/filepath" + "time" + + cron "github.com/robfig/cron/v3" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/leaderelection" + "k8s.io/client-go/tools/leaderelection/resourcelock" + cbc "k8s.io/component-base/config" + "k8s.io/klog/v2" + ctrlruntime "sigs.k8s.io/controller-runtime" +) + +const ( + ResourceName = "nebula-certificate-generator" +) + +type CertGenerator struct { + // LeaderElection defines the configuration of leader election client. + LeaderElection cbc.LeaderElectionConfiguration + + // WebhookNames represents the names of the webhooks in the webhook server (i.e. controller-manager-nebula-operator-webhook, autoscaler-nebula-operator-webhook) + WebhookNames *[]string + + // WebhookServerName represents the name of the webhook server associated with the certificate. + WebhookServerName string + + // WebhookNamespace represents the namespace of the webhook server associated with the certificate. + WebhookNamespace string + + // CertDir represents the directory to save the certificates in + CertDir string + + // CertValidity represents the number of days the certificate should be valid for + CertValidity int64 + + // SecretName represents the name of the secret used to store the webhook certificates + SecretName string + + // SecretNamespace represents the namespace of the secret used to store the webhook certificates + SecretNamespace string + + // KubernetesDomain represents the custom kubernetes domain needed in the certificate + KubernetesDomain string +} + +func (c *CertGenerator) Run(ctx context.Context) error { + klog.Info("Getting kubernetes configs") + cfg, err := ctrlruntime.GetConfig() + if err != nil { + panic(err) + } + + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + klog.Errorf("Error building Kubernetes clientset: %v", err.Error()) + return err + } + + // Initialize certificate + certValidityDuration := (time.Duration(c.CertValidity) * 24 * 60 * time.Minute) + err = c.rotateCert(ctx, clientset, certValidityDuration) + if err != nil { + klog.Errorf("Error rotating certificate for webhook [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + return err + } + + // Start background job for rotation + klog.Infof("Starting cert rotation cronjob for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + rotateJob := cron.New() + rotateJob.AddFunc(fmt.Sprintf("@every %v", certValidityDuration-1*time.Minute), func() { + klog.Infof("Rotating certificate for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + err := c.rotateCert(ctx, clientset, certValidityDuration+1*time.Minute) + if err != nil { + klog.Errorf("Error rotating certificate for webhook [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + os.Exit(1) + } + klog.Infof("Certifcate rotation complete for webhook [%v/%v]. Will rotate in %v", c.WebhookNamespace, c.WebhookServerName, certValidityDuration) + }) + rotateJob.Start() + klog.Infof("Cert rotation cronjob for webhook [%v/%v] started successfully", c.WebhookNamespace, c.WebhookServerName) + + return nil +} + +func (c *CertGenerator) rotateCert(ctx context.Context, clientset *kubernetes.Clientset, certValidity time.Duration) error { + klog.Info("Doing leader election") + id, err := os.Hostname() + if err != nil { + klog.Errorf("Failed to get hostname: %v", err) + return err + } + + rl, err := resourcelock.New( + c.LeaderElection.ResourceLock, + c.LeaderElection.ResourceNamespace, + ResourceName, + clientset.CoreV1(), + clientset.CoordinationV1(), + resourcelock.ResourceLockConfig{ + Identity: fmt.Sprintf("%v-%v", id, c.LeaderElection.ResourceName), + }, + ) + if err != nil { + klog.Errorf("Error creating resource lock: %v", err) + return err + } + + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{ + Lock: rl, + LeaseDuration: c.LeaderElection.LeaseDuration.Duration, + RenewDeadline: c.LeaderElection.RenewDeadline.Duration, + RetryPeriod: c.LeaderElection.RetryPeriod.Duration, + Callbacks: leaderelection.LeaderCallbacks{ + OnStartedLeading: func(ctx context.Context) { + klog.Info("Leader election successful. Starting certificate rotation") + + // Check certificate validity + klog.Infof("Checking certificate validity for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + currCertValidity, hasCert := c.getCertValidity("tls.crt") + + // Rotate if needed + if !hasCert { + klog.Infof("No certificate detected. Creating certificate for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + err := c.doCertRotation(clientset, certValidity) + if err != nil { + klog.Errorf("Error rotating certificate for webhook [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + os.Exit(1) + } + klog.Infof("Certificate created successfully for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + } else if currCertValidity <= 2*time.Minute { + klog.Infof("Certificate is within 2 min of expiration. Rotating certificate for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + err := c.doCertRotation(clientset, currCertValidity+(certValidity)) + if err != nil { + klog.Errorf("Error rotating certificate for webhook [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + os.Exit(1) + } + klog.Infof("Certificate rotated successfully for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + } else { + klog.Infof("Certificate for webhook [%v/%v] is still valid for %v. Skipping cert rotation for now", c.WebhookNamespace, c.WebhookServerName, currCertValidity) + } + + klog.Info("Finish certificate rotation. Relinquishing leadership") + cancel() + }, + OnStoppedLeading: func() { + klog.Info("Lost leadership, stopping") + }, + }, + ReleaseOnCancel: true, + }) + + return nil +} + +func (c *CertGenerator) doCertRotation(clientset *kubernetes.Clientset, certValidity time.Duration) error { + klog.Infof("Start generating certificates for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + caCert, caKey, err := c.generateCACert(certValidity) + if err != nil { + klog.Errorf("Error generating CA certificate for webhook server [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + return err + } + + serverCert, serverKey, err := c.generateServerCert(caCert, caKey, certValidity) + if err != nil { + klog.Errorf("Error generating server certificate for webhook server [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + return err + } + + err = c.updateSecret(clientset, serverCert, serverKey, caCert) + if err != nil { + klog.Errorf("Error updating secret for webhook server [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + return err + } + klog.Infof("Certificates generated successfully for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + + klog.Infof("Updating ca bundle for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + err = c.updateWebhookConfiguration(clientset, caCert) + if err != nil { + klog.Errorf("Error updating ca bundle for webhook server [%v/%v]: %v", c.WebhookNamespace, c.WebhookServerName, err) + return err + } + klog.Infof("Ca bundle updated successfully for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + + return nil +} + +func (c *CertGenerator) generateCACert(certValidity time.Duration) ([]byte, *ecdsa.PrivateKey, error) { + klog.V(4).Infof("Generating CA certificate and key for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + caKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, nil, err + } + + caTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Country: []string{"US"}, + CommonName: c.WebhookServerName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certValidity), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + BasicConstraintsValid: true, + } + + caCert, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + caPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert}) + + klog.V(4).Infof("CA certificate and key generated successfully for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + + return caPem, caKey, nil +} + +func (c *CertGenerator) generateServerCert(caCert []byte, caKey *ecdsa.PrivateKey, certValidity time.Duration) ([]byte, []byte, error) { + klog.V(4).Infof("Generating tls certificate and key for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + serverKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, nil, err + } + + dnsNames := make([]string, 2*len(*c.WebhookNames)) + for idx := -1; idx < len(*c.WebhookNames)-1; idx++ { + dnsNames[idx+1] = fmt.Sprintf("%v.%v.svc", (*c.WebhookNames)[idx+1], c.WebhookNamespace) + dnsNames[idx+2] = fmt.Sprintf("%v.%v.svc.%v", (*c.WebhookNames)[idx+1], c.WebhookNamespace, c.KubernetesDomain) + } + + serverTemplate := x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + Country: []string{"US"}, + CommonName: c.WebhookServerName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(certValidity), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: dnsNames, + } + + caBlock, _ := pem.Decode(caCert) + ca, err := x509.ParseCertificate(caBlock.Bytes) + if err != nil { + return nil, nil, err + } + + serverCert, err := x509.CreateCertificate(rand.Reader, &serverTemplate, ca, &serverKey.PublicKey, caKey) + if err != nil { + return nil, nil, err + } + + certPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: serverCert}) + marshalledKey, err := x509.MarshalECPrivateKey(serverKey) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling webhook server certificate key: %v", err) + } + + keyPem := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: marshalledKey}) + klog.V(4).Infof("TLS certificate and key generated successfully for webhook server [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + + return certPem, keyPem, nil +} + +func (c *CertGenerator) updateSecret(clientset *kubernetes.Clientset, certPEM, keyPEM, caPEM []byte) error { + klog.V(4).Infof("Updating secret [%v/%v] for webhook server [%v/%v]", c.SecretNamespace, c.SecretName, c.WebhookNamespace, c.WebhookServerName) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: c.SecretName, + Namespace: c.SecretNamespace, + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + "tls.crt": certPEM, + "tls.key": keyPEM, + "ca.crt": caPEM, + }, + } + + // The mounted local directory in the pod will automatically refresh regardless of the sync-interval since we're updating the data field in the secret here + _, err := clientset.CoreV1().Secrets(c.SecretNamespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update secret: %v", err) + } + + // Wait for certificate in local volume to update + err = c.waitForCertificateUpdate(filepath.Join(c.CertDir, "tls.crt"), certPEM) + if err != nil { + return fmt.Errorf("failed to wait for secret [%v/%v] to update: %v", c.SecretNamespace, c.SecretName, err) + } + + klog.V(4).Infof("secret [%v/%v] updated successfully for webhook server [%v/%v]", c.SecretNamespace, c.SecretName, c.WebhookNamespace, c.WebhookServerName) + + return nil +} + +func (c *CertGenerator) waitForCertificateUpdate(certPath string, expectedContent []byte) error { + checkInterval := 2 * time.Second + + for { + certBytes, err := os.ReadFile(certPath) + if err != nil { + return fmt.Errorf("unable to read certificate at path %v for webhook [%v/%v]: %v", certPath, c.WebhookNamespace, c.WebhookServerName, err) + } + + block, _ := pem.Decode(certBytes) + if block != nil && block.Type == "CERTIFICATE" && bytes.Contains(certBytes, expectedContent) { + klog.V(4).Infof("Certificate updated successfully in the local volume for webhook [%v/%v]", c.WebhookNamespace, c.WebhookServerName) + return nil + } + + klog.V(4).Infof("Waiting for certificate to be updated in the local volume for webhook [%v/%v], retrying in %v...", c.WebhookNamespace, c.WebhookServerName, checkInterval) + time.Sleep(checkInterval) + } +} + +func (c *CertGenerator) updateWebhookConfiguration(client *kubernetes.Clientset, caCert []byte) error { + webhook, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.Background(), c.WebhookServerName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("failed to get webhook configuration: %v", err) + } + + for i := range webhook.Webhooks { + webhook.Webhooks[i].ClientConfig.CABundle = caCert + } + + _, err = client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Update(context.Background(), webhook, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("failed to update webhook configuration: %v", err) + } + + return nil +} + +func (c *CertGenerator) getCertValidity(certName string) (time.Duration, bool) { + certPEM, err := os.ReadFile(filepath.Join(c.CertDir, certName)) + if err != nil { + return -1, false + } + + block, _ := pem.Decode(certPEM) + if block == nil || block.Type != "CERTIFICATE" { + return -1, false + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return -1, false + } + + return time.Until(cert.NotAfter), true +} diff --git a/pkg/flag/webhook/webhook.go b/pkg/flag/webhook/webhook.go index eaf078c9..0611c5fb 100644 --- a/pkg/flag/webhook/webhook.go +++ b/pkg/flag/webhook/webhook.go @@ -48,14 +48,35 @@ type Options struct { // CertName is the server certificate name. Defaults to tls.crt. CertName string + // CertValidity represents the number of days the certificate should be valid for + CertValidity int64 + // KeyName is the server key name. Defaults to tls.key. KeyName string + // KubernetesDomain represents the custom kubernetes domain needed in the certificate + KubernetesDomain string + + // SecretName represents the name of the secret used to store the webhook certificates + SecretName string + + // SecretNamespace represents the namespace of the secret used to store the webhook certificates + SecretNamespace string + // TLSMinVersion is the minimum version of TLS supported. Possible values: 1.0, 1.1, 1.2, 1.3. // Some environments have automated security scans that trigger on TLS versions or insecure cipher suites, and // setting TLS to 1.3 would solve both problems. // Defaults to 1.3. TLSMinVersion string + + // WebhookNames represents the names of the webhooks in the webhook server (i.e. controller-manager-nebula-operator-webhook, autoscaler-nebula-operator-webhook) + WebhookNames *[]string + + // WebhookServerName represents the name of the webhook server associated with the certificate. + WebhookServerName string + + // WebhookNamespace represents the namespace of the webhook server associated with the certificate. + WebhookNamespace string } func (o *Options) AddFlags(flags *pflag.FlagSet) { @@ -66,9 +87,16 @@ func (o *Options) AddFlags(flags *pflag.FlagSet) { flags.StringVar(&o.CertDir, "webhook-cert-dir", defaultCertDir, "The directory that contains the server key and certificate.") flags.StringVar(&o.CertName, "webhook-tls-cert-file-name", "tls.crt", "The name of server certificate.") + flags.Int64Var(&o.CertValidity, "certificate-validity", 365, "Specifies the number of days the certificate should be valid for") flags.StringVar(&o.KeyName, "webhook-tls-private-key-file-name", "tls.key", "The name of server key.") + flags.StringVar(&o.KubernetesDomain, "kube-domain", "cluster.local", "Specifies the namespace of the webhook to associate with the certificate") + flags.StringVar(&o.SecretName, "secret-name", "nebula-operator-webhook-secret", "Specifies the name of the webhook to associate with the certificate") + flags.StringVar(&o.SecretNamespace, "secret-namespace", "default", "Specifies the namespace of the webhook to associate with the certificate") flags.StringVar(&o.TLSMinVersion, "webhook-tls-min-version", defaultTLSMinVersion, "Minimum TLS version supported. Possible values: 1.0, 1.1, 1.2, 1.3.") + o.WebhookNames = flags.StringSlice("webhook-names", []string{}, "A comma-seperated list of the names of the webhooks supported by the webhook server (i.e. controller-manager-nebula-operator-webhook, autoscaler-nebula-operator-webhook)") + flags.StringVar(&o.WebhookServerName, "webhook-server-name", "nebulaWebhook", "Specifies the name of the webhook to associate with the certificate") + flags.StringVar(&o.WebhookNamespace, "webhook-namespace", "default", "Specifies the namespace of the webhook to associate with the certificate") } func (o *Options) Validate() field.ErrorList { diff --git a/pkg/webhook/autoscaler/helper.go b/pkg/webhook/autoscaler/helper.go new file mode 100644 index 00000000..3b122109 --- /dev/null +++ b/pkg/webhook/autoscaler/helper.go @@ -0,0 +1,62 @@ +/* +Copyright 2024 Vesoft 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 nebulaautoscaler + +import ( + "github.com/vesoft-inc/nebula-operator/apis/autoscaling/v1alpha1" + "github.com/vesoft-inc/nebula-operator/pkg/webhook/util/validation" + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/klog/v2" +) + +// ValidateNebulaAutoscalerCreate validates a NebulaAutoscaler on create. +func validateNebulaAutoscalerCreate(na *v1alpha1.NebulaAutoscaler) (allErrs field.ErrorList) { + name := na.Name + namespace := na.Namespace + + klog.Infof("receive admission with resource [%s/%s], GVK %s, operation %s", namespace, name, + na.GroupVersionKind().String(), admissionv1.Create) + + allErrs = append(allErrs, validateNebulaAutoscalarReplica(na)...) + + return allErrs +} + +// ValidateNebulaCluster validates a NebulaAutoscaler on Update. +func validateNebulaAutoscalerUpdate(na, oldNA *v1alpha1.NebulaAutoscaler) (allErrs field.ErrorList) { + name := na.Name + namespace := na.Namespace + + klog.Infof("receive admission with resource [%s/%s], GVK %s, operation %s", namespace, name, + na.GroupVersionKind().String(), admissionv1.Update) + + allErrs = append(allErrs, validateNebulaAutoscalarReplica(na)...) + + return allErrs +} + +// validateNebulaClusterGraphd validates the replicas in an NebulaAutoscaler +func validateNebulaAutoscalarReplica(na *v1alpha1.NebulaAutoscaler) (allErrs field.ErrorList) { + allErrs = append(allErrs, validation.ValidateMinMaxReplica( + field.NewPath("spec").Child("graphPolicy").Child("minReplicas"), + int(*na.Spec.GraphdPolicy.MinReplicas), + int(na.Spec.GraphdPolicy.MaxReplicas), + )...) + + return allErrs +} diff --git a/pkg/webhook/autoscaler/validating.go b/pkg/webhook/autoscaler/validating.go new file mode 100644 index 00000000..3a4bd234 --- /dev/null +++ b/pkg/webhook/autoscaler/validating.go @@ -0,0 +1,82 @@ +/* +Copyright 2024 Vesoft 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 nebulaautoscaler + +import ( + "context" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/vesoft-inc/nebula-operator/apis/autoscaling/v1alpha1" +) + +// ValidatingAdmission handles StatefulSet +type ValidatingAdmission struct { + // Decoder decodes objects + Decoder *admission.Decoder +} + +var _ admission.Handler = &ValidatingAdmission{} + +// Handle handles admission requests. +func (h *ValidatingAdmission) Handle(_ context.Context, req admission.Request) (resp admission.Response) { + klog.Infof("start validating resource %v [%s/%s] operation %s", req.Resource, req.Namespace, req.Name, req.Operation) + + defer func() { + klog.Infof("end validating, allowed %v, reason %v, message %s", resp.Allowed, + resp.Result.Reason, resp.Result.Message) + }() + + obj := &v1alpha1.NebulaAutoscaler{} + + if req.Operation == admissionv1.Delete { + if err := h.Decoder.DecodeRaw(req.OldObject, obj); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + } else { + if err := h.Decoder.Decode(req, obj); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + } + + operation := req.AdmissionRequest.Operation + + if operation == admissionv1.Connect { + return admission.ValidationResponse(true, "") + } + + if operation == admissionv1.Create { + if allErrs := validateNebulaAutoscalerCreate(obj); len(allErrs) > 0 { + return admission.Errored(http.StatusUnprocessableEntity, allErrs.ToAggregate()) + } + } else if operation == admissionv1.Update { + oldObj := &v1alpha1.NebulaAutoscaler{} + + if err := h.Decoder.DecodeRaw(req.AdmissionRequest.OldObject, oldObj); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + if allErrs := validateNebulaAutoscalerUpdate(obj, oldObj); len(allErrs) > 0 { + return admission.Errored(http.StatusUnprocessableEntity, allErrs.ToAggregate()) + } + } + + return admission.ValidationResponse(true, "") +} diff --git a/pkg/webhook/util/validation/autoscaler.go b/pkg/webhook/util/validation/autoscaler.go new file mode 100644 index 00000000..b7f16acb --- /dev/null +++ b/pkg/webhook/util/validation/autoscaler.go @@ -0,0 +1,28 @@ +/* +Copyright 2024 Vesoft 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 validation + +import "k8s.io/apimachinery/pkg/util/validation/field" + +// ValidateMinReplicasGraphd validates replicas min value for Graphd +func ValidateMinMaxReplica(fldPath *field.Path, minReplicas, maxReplicas int) (allErrs field.ErrorList) { + if fieldErr := ValidateMinLessThanMax(fldPath, minReplicas, maxReplicas); fieldErr != nil { + allErrs = append(allErrs, fieldErr) + } + + return allErrs +} diff --git a/pkg/webhook/util/validation/validation.go b/pkg/webhook/util/validation/validation.go index 6a0dedd4..914c4a14 100644 --- a/pkg/webhook/util/validation/validation.go +++ b/pkg/webhook/util/validation/validation.go @@ -47,3 +47,11 @@ func ValidateOddNumber(fldPath *field.Path, value int) *field.Error { } return nil } + +// ValidateMinLessThanMax validates that the minimum replicas is less then or equal to the maximum replicas +func ValidateMinLessThanMax(fldPath *field.Path, minReplicas, maxReplicas int) *field.Error { + if minReplicas > maxReplicas { + return field.Invalid(fldPath, minReplicas, fmt.Sprintf("min replica %v should be less than or equal to max replicas %v", minReplicas, maxReplicas)) + } + return nil +}