From 55aba37df60fcc98220d3e782dd6f1fc32c42623 Mon Sep 17 00:00:00 2001 From: Drew Wells Date: Fri, 27 Sep 2024 08:46:56 -0500 Subject: [PATCH] PTEUDO-1449 PTEUDO-1385 update mutatingwebhook API to consider dsnexec and dbproxy (#320) new field to declare if you want dbproxy, dsnexec or both install xnr in a separate group, which is unused in code for now add tests for dsnexec webhook --- Dockerfile | 2 - cmd/main.go | 24 +-- config/dsnexec/dsnexecsidecar.json | 35 ---- dsnexec/pkg/dsnexec/dsnexec.go | 13 +- helm/db-controller/templates/_helpers.tpl | 7 + helm/db-controller/templates/deployment.yaml | 2 - .../mutatingwebhookconfiguration.yaml | 36 ++-- .../db-controller/templates/test/dbproxy.yaml | 7 +- .../db-controller/templates/test/dsnexec.yaml | 122 ++++++++++++ helm/db-controller/templates/xrd.yaml | 6 +- helm/db-controller/values.yaml | 18 +- internal/webhook/dbproxy.go | 126 ++---------- internal/webhook/dbproxy_test.go | 44 ++--- internal/webhook/defaulter.go | 173 +++++++++++++++++ internal/webhook/dsnexec.go | 125 ++++++++++++ internal/webhook/dsnexec_test.go | 182 ++++++++++++++++++ internal/webhook/suite_test.go | 6 +- webhook/webhook-dsnexec.go | 120 ------------ 18 files changed, 699 insertions(+), 349 deletions(-) delete mode 100644 config/dsnexec/dsnexecsidecar.json create mode 100644 helm/db-controller/templates/test/dsnexec.yaml create mode 100644 internal/webhook/defaulter.go create mode 100644 internal/webhook/dsnexec.go create mode 100644 internal/webhook/dsnexec_test.go delete mode 100644 webhook/webhook-dsnexec.go diff --git a/Dockerfile b/Dockerfile index b9096bf0..0aff5c4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,6 @@ COPY api/ api/ COPY internal/controller/ internal/controller/ COPY internal/webhook/ internal/webhook/ COPY pkg/ pkg/ -# FIXME: move this to pkg/ -COPY webhook/ webhook/ # FIXME: config is for raw manifest yaml that we use in helm, remove this when possible COPY config/ config/ diff --git a/cmd/main.go b/cmd/main.go index 10782260..4d0157bd 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,7 +35,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" persistancev1 "github.com/infobloxopen/db-controller/api/v1" "github.com/infobloxopen/db-controller/internal/controller" @@ -44,7 +43,6 @@ import ( "github.com/infobloxopen/db-controller/pkg/databaseclaim" "github.com/infobloxopen/db-controller/pkg/rdsauth" "github.com/infobloxopen/db-controller/pkg/roleclaim" - dbwebhook "github.com/infobloxopen/db-controller/webhook" persistanceinfobloxcomv1alpha1 "github.com/infobloxopen/db-controller/api/persistance.infoblox.com/v1alpha1" // +kubebuilder:scaffold:imports @@ -96,7 +94,6 @@ func main() { var metricsDepYamlPath string var metricsConfigYamlPath string var enableDBProxyWebhook bool - var enableDSNExecWebhook bool flag.StringVar(&class, "class", "default", "The class of claims this db-controller instance needs to address.") @@ -105,7 +102,6 @@ func main() { flag.StringVar(&metricsDepYamlPath, "metrics-dep-yaml", "/config/postgres-exporter/deployment.yaml", "path to the metrics deployment yaml") flag.StringVar(&metricsConfigYamlPath, "metrics-config-yaml", "/config/postgres-exporter/config.yaml", "path to the metrics config yaml") flag.BoolVar(&enableDBProxyWebhook, "enable-db-proxy", false, "Enable DB Proxy webhook. Enabling this option will cause the db-controller to inject db proxy pod into pods with the infoblox.com/db-secret-path annotation set.") - flag.BoolVar(&enableDSNExecWebhook, "enable-dsnexec", false, "Enable Dsnexec webhook. Enabling this option will cause the db-controller to inject dsnexec container into pods with the infoblox.com/remote-db-dsn-secret and infoblox.com/dsnexec-config-secret annotations set.") opts := zap.Options{ Development: true, @@ -247,30 +243,12 @@ func main() { Namespace: namespace, Class: class, DBProxyImg: os.Getenv("DBPROXY_IMAGE"), + DSNExecImg: os.Getenv("DSNEXEC_IMAGE"), }); err != nil { setupLog.Error(err, "failed to setup webhooks") os.Exit(1) } } - if enableDSNExecWebhook { - - cfg, err := dbwebhook.ParseConfig(dsnExecSidecarConfigPath) - - if err != nil { - setupLog.Error(err, "could not parse dsnexec sidecar configuration") - os.Exit(1) - } - setupLog.Info("dnsexec-controller", "config", cfg) - - mgr.GetWebhookServer().Register("/mutate-dsnexec", &webhook.Admission{ - Handler: &dbwebhook.DsnExecInjector{ - Name: "Dsnexec", - Client: mgr.GetClient(), - DsnExecSidecarConfig: cfg, - Decoder: admission.NewDecoder(mgr.GetScheme()), - }, - }) - } setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { diff --git a/config/dsnexec/dsnexecsidecar.json b/config/dsnexec/dsnexecsidecar.json deleted file mode 100644 index 44a43675..00000000 --- a/config/dsnexec/dsnexecsidecar.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "containers": [ - { - "imagePullPolicy": "IfNotPresent", - "args": ["run","-c","/var/run/dsn-exec/config.yaml"], - "name": "dsn-exec", - "volumeMounts": [ - { - "mountPath": "/var/run/db-dsn", - "name": "remote-db-dsn-volume" - }, - { - "mountPath": "/var/run/dsn-exec", - "name": "dsnexec-config-volume" - } - ] - } - ], - "volumes": [ - { - "name": "remote-db-dsn-volume", - "secret": { - "optional": false, - "secretName": "..." - } - }, - { - "name": "dsnexec-config-volume", - "secret": { - "optional": false, - "secretName": "..." - } - } - ] -} \ No newline at end of file diff --git a/dsnexec/pkg/dsnexec/dsnexec.go b/dsnexec/pkg/dsnexec/dsnexec.go index a015066c..06f01a06 100644 --- a/dsnexec/pkg/dsnexec/dsnexec.go +++ b/dsnexec/pkg/dsnexec/dsnexec.go @@ -5,6 +5,7 @@ import ( "database/sql" "fmt" "math/bits" + "net/url" "strconv" "strings" "sync" @@ -13,6 +14,7 @@ import ( "github.com/Masterminds/sprig/v3" _ "github.com/infobloxopen/db-controller/dsnexec/pkg/shelldb" _ "github.com/lib/pq" + log "github.com/sirupsen/logrus" ) // Hanlder is an instance of dsnexec. @@ -85,14 +87,20 @@ func (w *Handler) exec() error { parsedOpts, err := parse(source.DSN) if err != nil { - return fmt.Errorf("failed to parse dsn: %v", err) + return fmt.Errorf("failed to parse dsn: %s", err) } parsedOpts["raw_dsn"] = source.DSN argContext[name] = parsedOpts } + + dsnURL, err := url.Parse(w.config.Destination.DSN) + if err == nil { + log.Infof("destination dsn: %s", dsnURL.Redacted()) + } + db, err := sql.Open(w.config.Destination.Driver, w.config.Destination.DSN) if err != nil { - return fmt.Errorf("failed to open destination database: %v", err) + return fmt.Errorf("failed to open destination database: %s", err) } defer db.Close() @@ -108,6 +116,7 @@ func (w *Handler) exec() error { cmd := bs.String() if len(v.Args) == 0 { if _, err := db.Exec(cmd); err != nil { + log.Infof("failed to execute sql command: %s err: %s", cmd, err) return fmt.Errorf("failed to execute sql: %v", err) } continue diff --git a/helm/db-controller/templates/_helpers.tpl b/helm/db-controller/templates/_helpers.tpl index c3c74413..a7b15b4c 100644 --- a/helm/db-controller/templates/_helpers.tpl +++ b/helm/db-controller/templates/_helpers.tpl @@ -60,3 +60,10 @@ Create the name of the service account to use {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} + +{{- define "db-controller.group" -}} +{{- if not (eq .Values.dbController.class "default") -}} + {{- .Values.dbController.class -}}. +{{- end -}} +persistance.infoblox.com +{{- end }} diff --git a/helm/db-controller/templates/deployment.yaml b/helm/db-controller/templates/deployment.yaml index a7c50e70..da62e83d 100644 --- a/helm/db-controller/templates/deployment.yaml +++ b/helm/db-controller/templates/deployment.yaml @@ -52,10 +52,8 @@ spec: - --health-probe-bind-address=:{{ .Values.healthProbe.port }} - --leader-elect - --enable-db-proxy={{ .Values.dbproxy.enabled }} - - --enable-dsnexec={{ .Values.dsnexec.enabled }} - --config-file=/etc/config/config.yaml - --dsnexec-sidecar-config-path=config/dsnexec/dsnexecsidecar.json - - --db-identifier-prefix={{ tpl .Values.db.identifier.prefix . }} - --class={{ .Values.dbController.class }} - -zap-encoder={{ .Values.zapLogger.encoding }} - -zap-log-level={{ .Values.zapLogger.level }} diff --git a/helm/db-controller/templates/mutatingwebhookconfiguration.yaml b/helm/db-controller/templates/mutatingwebhookconfiguration.yaml index afe7dbcd..2b8b5d99 100644 --- a/helm/db-controller/templates/mutatingwebhookconfiguration.yaml +++ b/helm/db-controller/templates/mutatingwebhookconfiguration.yaml @@ -1,4 +1,4 @@ -{{- if or ( .Values.dbproxy.enabled ) ( .Values.dsnexec.enabled ) }} +{{- if .Values.dbproxy.enabled }} apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: @@ -6,7 +6,6 @@ metadata: annotations: cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "db-controller.fullname" . }}-webhook webhooks: -{{- if .Values.dbproxy.enabled }} - clientConfig: service: name: {{ include "db-controller.fullname" . }} @@ -16,11 +15,16 @@ webhooks: sideEffects: None admissionReviewVersions: ["v1"] failurePolicy: Fail - name: persistance.atlas.infoblox.com + name: dbproxy.persistance.atlas.infoblox.com objectSelector: matchExpressions: - - key: "persistance.atlas.infoblox.com/databaseclaim" + # This will locate a databaseclaim or a dbroleclaim + - key: "persistance.atlas.infoblox.com/claim" operator: "Exists" + - key: "persistance.atlas.infoblox.com/dbproxy" + operator: "In" + values: + - "enabled" # Important to prevent multiple db-controllers from stepping on each other - key: "persistance.atlas.infoblox.com/class" operator: "In" @@ -37,25 +41,30 @@ webhooks: resources: - pods scope: "Namespaced" -{{- end }} -{{- if .Values.dsnexec.enabled }} - clientConfig: service: name: {{ include "db-controller.fullname" . }} - path: /mutate-dsnexec + path: /mutate--v1-pod port: 9443 namespace: {{ .Release.Namespace }} sideEffects: None admissionReviewVersions: ["v1"] - failurePolicy: Ignore - name: dsnexec-injector.infoblox.com + failurePolicy: Fail + name: dsnexec.persistance.atlas.infoblox.com objectSelector: matchExpressions: - # Disable dsn-exec until we reimplement it in #PTEUDO-1385 - - key: "donotenablethiswithoutwritingtests" - Operator: "In" + # This will locate a databaseclaim or a dbroleclaim + - key: "persistance.atlas.infoblox.com/claim" + operator: "Exists" + - key: "persistance.atlas.infoblox.com/dsnexec" + operator: "In" values: - - "mytestsareallpassing" + - "enabled" + # Important to prevent multiple db-controllers from stepping on each other + - key: "persistance.atlas.infoblox.com/class" + operator: "In" + values: + - {{ .Values.dbController.class | quote }} rules: - apiGroups: - "" @@ -68,4 +77,3 @@ webhooks: - pods scope: "Namespaced" {{- end }} -{{- end }} diff --git a/helm/db-controller/templates/test/dbproxy.yaml b/helm/db-controller/templates/test/dbproxy.yaml index 4d172456..f6ccba11 100644 --- a/helm/db-controller/templates/test/dbproxy.yaml +++ b/helm/db-controller/templates/test/dbproxy.yaml @@ -118,7 +118,7 @@ metadata: spec: containers: - name: postgres - image: postgres:15 + image: {{ .Values.tools.postgres.repository }}:{{ .Values.tools.postgres.tag }} env: - name: POSTGRES_USER value: "myuser" @@ -130,12 +130,13 @@ spec: apiVersion: v1 kind: Pod metadata: - name: {{ .Release.Name }}-dbproxy-test-proxy + name: {{ .Release.Name }}-dbproxy-test namespace: {{ .Release.Namespace }} labels: {{- include "db-controller.labels" . | nindent 4 }} - persistance.atlas.infoblox.com/databaseclaim: {{ .Release.Name }}-dbproxy-test + persistance.atlas.infoblox.com/claim: {{ .Release.Name }}-dbproxy-test persistance.atlas.infoblox.com/class: {{ .Values.dbController.class | quote }} + persistance.atlas.infoblox.com/dbproxy: enabled annotations: helm.sh/hook: test helm.sh/hook-delete-policy: "before-hook-creation,hook-succeeded" diff --git a/helm/db-controller/templates/test/dsnexec.yaml b/helm/db-controller/templates/test/dsnexec.yaml new file mode 100644 index 00000000..8c892c9e --- /dev/null +++ b/helm/db-controller/templates/test/dsnexec.yaml @@ -0,0 +1,122 @@ +{{ $tableName := printf "dnsexec_%s" (randAlpha 4 | lower) }} +--- +apiVersion: v1 +kind: Pod +metadata: + name: {{ .Release.Name }}-dsnexec-test + namespace: {{ .Release.Namespace }} + labels: + {{- include "db-controller.labels" . | nindent 4 }} + persistance.atlas.infoblox.com/claim: {{ .Release.Name }}-dbproxy-test + persistance.atlas.infoblox.com/class: {{ .Values.dbController.class | quote }} + persistance.atlas.infoblox.com/dsnexec: enabled + persistance.atlas.infoblox.com/dsnexec-config: {{ .Release.Name }}-dsnexec-config + annotations: + helm.sh/hook: test + helm.sh/hook-delete-policy: "before-hook-creation,hook-succeeded" +spec: + serviceAccountName: {{ .Release.Name }}-dbproxy-test + initContainers: + - name: init + image: postgres:15 + env: + - name: PGCONNECT_TIMEOUT + value: "2" + command: + - /bin/sh + - -c + - | + ls /etc/secrets + cat /etc/secrets/uri_dsn.txt + for i in $(seq 1 10); do + echo "Attempt $i: Connecting to PostgreSQL..." + if psql $(cat /etc/secrets/uri_dsn.txt) -c 'SELECT 1'; then + echo "Connection successful!" + exit 0 + fi + echo "Failed to connect. Retrying in 5 seconds..." + sleep 5 + done + echo "Failed to connect after 20 attempts. Exiting." + exit 1 + volumeMounts: + - name: dsn-volume + mountPath: /etc/secrets + readOnly: true + containers: + - name: wait + image: {{ .Values.tools.postgres.repository }}:{{ .Values.tools.postgres.tag }} + securityContext: + runAsUser: 0 + command: + - /bin/bash + - -cx + - | + echo "Waiting for dsnexec to run..." + # Table name to check + TABLE_NAME={{ $tableName }} + # Function to check if the table exists + check_table_exists() { + # Use PSQL with the connection string to check if the table exists + psql $(cat /etc/secrets/uri_dsn.txt) -tAc "SELECT to_regclass('$TABLE_NAME');" | grep -q "$TABLE_NAME" + return $? + } + + start_time=$(date +%s) + + # Loop for 1 minute (60 seconds) + while [ $(($(date +%s) - start_time)) -lt 60 ]; do + + if check_table_exists; then + echo "Table $TABLE_NAME exists!" + kill -s INT $(pidof dsnexec) + exit 0 + else + echo "Table $TABLE_NAME does not exist. Checking again in 5 seconds..." + sleep 5 + fi + done + + exit 1 + volumeMounts: + - name: dsn-volume + mountPath: /etc/secrets + readOnly: true + shareProcessNamespace: true + # Sidecar has a liveness probe, allow it to restart + restartPolicy: Never + volumes: + - name: dsn-volume + secret: + secretName: {{ .Release.Name }}-dbproxy-test +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ .Release.Name }}-dsnexec-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "db-controller.labels" . | nindent 4 }} + annotations: + # ensure this runs on the new db-controller, not the existing one + "helm.sh/hook": "test" + "helm.sh/hook-weight": "-5" +type: Opaque +stringData: + config.yaml: | + configs: + sql: + disabled: false + sources: + - driver: postgres + # This is not supported in code + filename: /var/run/db-dsn/dsn.txt + destination: + driver: "postgres" + dsn: {{ printf "postgres://myuser:mypassword@%s-dbproxy-test-db.%s.svc:5432/mydb?sslmode=disable" .Release.Name .Release.Namespace | quote }} + commands: + - command: |- + -- FIXME: The random tableName isn't unique across multiple helm test runs + DROP TABLE IF EXISTS {{ $tableName }}; + CREATE TABLE {{ $tableName }} ( first_column text ); +--- diff --git a/helm/db-controller/templates/xrd.yaml b/helm/db-controller/templates/xrd.yaml index 5dc640cb..81d47748 100644 --- a/helm/db-controller/templates/xrd.yaml +++ b/helm/db-controller/templates/xrd.yaml @@ -1,16 +1,18 @@ apiVersion: apiextensions.crossplane.io/v1 kind: CompositeResourceDefinition metadata: - name: xnetworkrecords.persistance.infoblox.com + name: xnetworkrecords.{{ include "db-controller.group" . }} labels: kustomize.toolkit.fluxcd.io/prune: disabled spec: - group: persistance.infoblox.com + group: {{ include "db-controller.group" . }} names: kind: XNetworkRecord plural: xnetworkrecords + {{- if eq .Values.dbController.class "default" }} shortNames: - xnr + {{- end }} versions: - name: v1alpha1 served: true diff --git a/helm/db-controller/values.yaml b/helm/db-controller/values.yaml index 216428d8..fe734b6d 100644 --- a/helm/db-controller/values.yaml +++ b/helm/db-controller/values.yaml @@ -17,12 +17,6 @@ db: dbController: class: default -tools: - kubectl: - repository: bitnami/kubectl - pullPolicy: IfNotPresent - tag: 1.28.5 - image: repository: ghcr.io/infobloxopen/db-controller pullPolicy: Always @@ -106,6 +100,16 @@ pdb: postgresql: enabled: false +tools: + postgres: + repository: "postgres" + tag: "15" + + kubectl: + repository: bitnami/kubectl + pullPolicy: IfNotPresent + tag: 1.28.5 + dbproxy: enabled: true image: @@ -114,8 +118,6 @@ dbproxy: tag: "" dsnexec: - # Do not enable this without writing complete unit and integration tests using helm - enabled: false image: repository: "ghcr.io/infobloxopen/dsnexec" tag: "" diff --git a/internal/webhook/dbproxy.go b/internal/webhook/dbproxy.go index b68692cd..3d9edbf2 100644 --- a/internal/webhook/dbproxy.go +++ b/internal/webhook/dbproxy.go @@ -4,118 +4,16 @@ import ( "context" "fmt" - v1 "github.com/infobloxopen/db-controller/api/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" ) var ( - // Set to {key}=disabled to disable injection - LabelCheck = "persistance.atlas.infoblox.com/databaseclaim" - AnnotationInjected = "persistance.atlas.infoblox.com/injected" - MountPath = "/dbproxy" - VolumeName = "dbproxydsn" - ContainerName = "dbproxy" - SecretKey = v1.DSNURIKey + MountPathProxy = "/dbproxy" + VolumeNameProxy = "dbproxydsn" + ContainerNameProxy = "dbproxy" ) -// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=persistance.atlas.infoblox.com,sideEffects=None,admissionReviewVersions=v1 - -// (for webhooks the path is of format /mutate---. Since this documentation uses Pod from the core API group, the group needs to be an empty string). - -// podAnnotator annotates Pods -type podAnnotator struct { - class string - namespace string - dbProxyImg string - k8sClient client.Reader -} - -type SetupConfig struct { - Namespace string - Class string - DBProxyImg string -} - -func SetupWebhookWithManager(mgr ctrl.Manager, cfg SetupConfig) error { - - if cfg.Namespace == "" { - return fmt.Errorf("namespace must be set") - } - - if cfg.Class == "" { - return fmt.Errorf("class must be set") - } - - if cfg.DBProxyImg == "" { - return fmt.Errorf("dbproxy image must be set") - } - - return ctrl.NewWebhookManagedBy(mgr). - For(&corev1.Pod{}). - WithDefaulter(&podAnnotator{ - namespace: cfg.Namespace, - class: cfg.Class, - dbProxyImg: cfg.DBProxyImg, - k8sClient: mgr.GetClient(), - }). - Complete() -} - -func (p *podAnnotator) Default(ctx context.Context, obj runtime.Object) error { - - pod, ok := obj.(*corev1.Pod) - if !ok { - logf.FromContext(ctx).Error(fmt.Errorf("expected a Pod but got a %T", obj), "failed to default") - return fmt.Errorf("expected a Pod but got a %T", obj) - } - - nn := types.NamespacedName{ - Namespace: pod.Namespace, - Name: pod.Name, - } - - log := logf.FromContext(ctx).WithName("dbproxy-defaulter").WithValues("pod", nn) - log.Info("processing") - - if pod.Labels == nil || len(pod.Labels[LabelCheck]) == 0 { - log.Info("Skipping Pod") - return nil - } - - claimName := pod.Labels[LabelCheck] - - var claim v1.DatabaseClaim - if err := p.k8sClient.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: claimName}, &claim); err != nil { - log.Info("unable to find databaseclaim", "claimName", claimName, "error", err) - return fmt.Errorf("unable to find databaseclaim %q check label %s: %s", claimName, LabelCheck, err) - } - - // This is the secret that db-controller manages - secretName := claim.Spec.SecretName - - if secretName == "" { - return fmt.Errorf("claim %q does not have secret name, this may resolve on its own", claimName) - } - - if *claim.Spec.Class != p.class { - log.Info("Skipping Pod, class mismatch", "claimClass", *claim.Spec.Class, "class", p.class) - return nil - } - - err := mutatePod(ctx, pod, secretName, p.dbProxyImg) - if err == nil { - log.Info("mutated_pod") - } - return nil -} - -func mutatePod(ctx context.Context, pod *corev1.Pod, secretName string, dbProxyImg string) error { +func mutatePodProxy(ctx context.Context, pod *corev1.Pod, secretName string, dbProxyImg string) error { if pod.Annotations == nil { pod.Annotations = map[string]string{} @@ -124,7 +22,7 @@ func mutatePod(ctx context.Context, pod *corev1.Pod, secretName string, dbProxyI // Process volume, check if existing var foundVolume bool for _, v := range pod.Spec.Volumes { - if v.Name == VolumeName { + if v.Name == VolumeNameProxy { foundVolume = true continue } @@ -132,7 +30,7 @@ func mutatePod(ctx context.Context, pod *corev1.Pod, secretName string, dbProxyI if !foundVolume { pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: VolumeName, + Name: VolumeNameProxy, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: secretName, @@ -143,19 +41,19 @@ func mutatePod(ctx context.Context, pod *corev1.Pod, secretName string, dbProxyI var foundContainer bool for _, c := range pod.Spec.Containers { - if c.Name == ContainerName { + if c.Name == ContainerNameProxy { foundContainer = true break } } if foundContainer { - pod.Annotations[AnnotationInjected] = "true" + pod.Annotations[AnnotationInjectedProxy] = "true" return nil } pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ - Name: ContainerName, + Name: ContainerNameProxy, Image: dbProxyImg, ImagePullPolicy: corev1.PullIfNotPresent, @@ -200,14 +98,14 @@ func mutatePod(ctx context.Context, pod *corev1.Pod, secretName string, dbProxyI }, VolumeMounts: []corev1.VolumeMount{ { - Name: VolumeName, - MountPath: MountPath, + Name: VolumeNameProxy, + MountPath: MountPathProxy, ReadOnly: true, }, }, }) - pod.Annotations[AnnotationInjected] = "true" + pod.Annotations[AnnotationInjectedProxy] = "true" return nil } diff --git a/internal/webhook/dbproxy_test.go b/internal/webhook/dbproxy_test.go index c90338c3..3fcf6380 100644 --- a/internal/webhook/dbproxy_test.go +++ b/internal/webhook/dbproxy_test.go @@ -13,7 +13,7 @@ import ( v1 "github.com/infobloxopen/db-controller/api/v1" ) -var _ = Describe("pod webhook defaulting", func() { +var _ = Describe("dbproxy defaulting", func() { BeforeEach(func() { By("create dependent resources") @@ -69,11 +69,11 @@ var _ = Describe("pod webhook defaulting", func() { By("Check pod is mutated") name := "annotation-enabled" - Expect(k8sClient.Create(ctx, makePod(name, "default-db"))).NotTo(HaveOccurred()) + Expect(k8sClient.Create(ctx, makePodProxy(name, "default-db"))).NotTo(HaveOccurred()) pod := &corev1.Pod{} err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) Expect(err).NotTo(HaveOccurred()) - Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjected, "true")) + Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjectedProxy, "true")) }) @@ -82,18 +82,18 @@ var _ = Describe("pod webhook defaulting", func() { name := "annotation-disabled" - Expect(k8sClient.Create(ctx, makePod(name, ""))).NotTo(HaveOccurred()) + Expect(k8sClient.Create(ctx, makePodProxy(name, ""))).NotTo(HaveOccurred()) pod := &corev1.Pod{} err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) Expect(err).NotTo(HaveOccurred()) - Expect(pod.Annotations).NotTo(HaveKeyWithValue(AnnotationInjected, "true")) + Expect(pod.Annotations).NotTo(HaveKeyWithValue(AnnotationInjectedProxy, "true")) name = "annotation-false" - Expect(k8sClient.Create(ctx, makePod(name, ""))).NotTo(HaveOccurred()) + Expect(k8sClient.Create(ctx, makePodProxy(name, ""))).NotTo(HaveOccurred()) err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) Expect(err).NotTo(HaveOccurred()) - Expect(pod.Annotations).NotTo(HaveKeyWithValue(AnnotationInjected, "true")) + Expect(pod.Annotations).NotTo(HaveKeyWithValue(AnnotationInjectedProxy, "true")) Expect(pod.Spec.Volumes).To(HaveLen(0)) Expect(pod.Spec.Containers).To(HaveLen(1)) @@ -102,49 +102,48 @@ var _ = Describe("pod webhook defaulting", func() { It("check initial volume and sidecar pod mutation", func() { name := "annotation-enabled" - Expect(k8sClient.Create(ctx, makePod(name, "default-db"))).NotTo(HaveOccurred()) + Expect(k8sClient.Create(ctx, makePodProxy(name, "default-db"))).NotTo(HaveOccurred()) pod := &corev1.Pod{} err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) Expect(err).NotTo(HaveOccurred()) - Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjected, "true")) + Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjectedProxy, "true")) By("Check secret volume") Expect(pod.Spec.Volumes).To(HaveLen(1)) - Expect(pod.Spec.Volumes[0].Name).To(Equal(VolumeName)) + Expect(pod.Spec.Volumes[0].Name).To(Equal(VolumeNameProxy)) Expect(pod.Spec.Volumes[0].VolumeSource.Secret.SecretName).To(Equal("test")) Expect(pod.Spec.Volumes[0].VolumeSource.Secret.Optional).To(BeNil()) By("Check sidecar pod is injected") sidecar := pod.Spec.Containers[len(pod.Spec.Containers)-1] - Expect(sidecar.Image).To(Equal(sidecarImage)) + Expect(sidecar.Image).To(Equal(sidecarImageProxy)) Expect(sidecar.VolumeMounts).To(HaveLen(1)) - Expect(sidecar.VolumeMounts[0].Name).To(Equal(VolumeName)) - Expect(sidecar.VolumeMounts[0].MountPath).To(Equal(MountPath)) + Expect(sidecar.VolumeMounts[0].Name).To(Equal(VolumeNameProxy)) + Expect(sidecar.VolumeMounts[0].MountPath).To(Equal(MountPathProxy)) Expect(sidecar.VolumeMounts[0].ReadOnly).To(BeTrue()) }) It("pre-mutated pods are not re-mutated", func() { name := "annotation-enabled" - Expect(k8sClient.Create(ctx, makeMutatedPod(name, "default-db", "test"))).NotTo(HaveOccurred()) + Expect(k8sClient.Create(ctx, makeMutatedPodProxy(name, "default-db", "test"))).NotTo(HaveOccurred()) pod := &corev1.Pod{} err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) Expect(err).NotTo(HaveOccurred()) By("Check pod has one set of volumes and sidecars") - Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjected, "true")) + Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjectedProxy, "true")) Expect(pod.Spec.Volumes).To(HaveLen(1)) Expect(pod.Spec.Containers).To(HaveLen(2)) }) }) -func makeMutatedPod(name, claimName, secretName string) *corev1.Pod { - pod := makePod(name, claimName) - Expect(mutatePod(context.TODO(), pod, secretName, sidecarImage)).To(Succeed()) +func makeMutatedPodProxy(name, claimName, secretName string) *corev1.Pod { + pod := makePodProxy(name, claimName) + Expect(mutatePodProxy(context.TODO(), pod, secretName, sidecarImageProxy)).To(Succeed()) return pod - } -func makePod(name, claimName string) *corev1.Pod { +func makePodProxy(name, claimName string) *corev1.Pod { pod := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -163,13 +162,14 @@ func makePod(name, claimName string) *corev1.Pod { switch name { case "annotation-enabled": pod.Labels = map[string]string{ - LabelCheck: claimName, + LabelCheckProxy: "enabled", + LabelClaim: claimName, } case "annotation-disabled": pod.Labels = map[string]string{} case "annotation-false": pod.Labels = map[string]string{ - LabelCheck: "", + LabelCheckProxy: "disabled", } } diff --git a/internal/webhook/defaulter.go b/internal/webhook/defaulter.go new file mode 100644 index 00000000..eb8bffba --- /dev/null +++ b/internal/webhook/defaulter.go @@ -0,0 +1,173 @@ +package webhook + +import ( + "context" + "fmt" + + v1 "github.com/infobloxopen/db-controller/api/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +var ( + // Set to {key}=disabled to disable injection + LabelCheckProxy = "persistance.atlas.infoblox.com/dbproxy" + LabelCheckExec = "persistance.atlas.infoblox.com/dsnexec" + LabelConfigExec = "persistance.atlas.infoblox.com/dsnexec-config" + LabelClaim = "persistance.atlas.infoblox.com/claim" + AnnotationInjectedProxy = "persistance.atlas.infoblox.com/injected-dbproxy" + AnnotationInjectedExec = "persistance.atlas.infoblox.com/injected-dsnexec" + SecretKey = v1.DSNURIKey +) + +// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=persistance.atlas.infoblox.com,sideEffects=None,admissionReviewVersions=v1 + +// (for webhooks the path is of format /mutate---. Since this documentation uses Pod from the core API group, the group needs to be an empty string). + +// podAnnotator annotates Pods +type podAnnotator struct { + class string + namespace string + dbProxyImg string + dsnExecImg string + k8sClient client.Reader +} + +type SetupConfig struct { + Namespace string + Class string + DBProxyImg string + DSNExecImg string +} + +func SetupWebhookWithManager(mgr ctrl.Manager, cfg SetupConfig) error { + + if cfg.Namespace == "" { + return fmt.Errorf("namespace must be set") + } + + if cfg.Class == "" { + return fmt.Errorf("class must be set") + } + + if cfg.DBProxyImg == "" { + return fmt.Errorf("dbproxy image must be set") + } + + if cfg.DSNExecImg == "" { + return fmt.Errorf("dsnexec image must be set") + } + + return ctrl.NewWebhookManagedBy(mgr). + For(&corev1.Pod{}). + WithDefaulter(&podAnnotator{ + namespace: cfg.Namespace, + class: cfg.Class, + dbProxyImg: cfg.DBProxyImg, + dsnExecImg: cfg.DSNExecImg, + k8sClient: mgr.GetClient(), + }). + Complete() +} + +func (p *podAnnotator) Default(ctx context.Context, obj runtime.Object) error { + + pod, ok := obj.(*corev1.Pod) + if !ok { + logf.FromContext(ctx).Error(fmt.Errorf("expected a Pod but got a %T", obj), "failed to default") + return fmt.Errorf("expected a Pod but got a %T", obj) + } + + nn := types.NamespacedName{ + Namespace: pod.Namespace, + Name: pod.Name, + } + + log := logf.FromContext(ctx).WithName("defaulter").WithValues("pod", nn) + log.Info("processing") + + if pod.Labels == nil || len(pod.Labels[LabelClaim]) == 0 { + log.Info("Skipping Pod") + return nil + } + + claimName := pod.Labels[LabelClaim] + + var claim v1.DatabaseClaim + if err := p.k8sClient.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: claimName}, &claim); err != nil { + log.Info("unable to find databaseclaim", "claimName", claimName, "error", err) + // return fmt.Errorf("unable to find databaseclaim %q check label %s: %s", claimName, LabelClaim, err) + } + var role v1.DbRoleClaim + if err := p.k8sClient.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: claimName}, &role); err != nil { + log.Info("unable to find roleclaim", "claimName", claimName, "error", err) + // return fmt.Errorf("unable to find databaseclaim %q check label %s: %s", claimName, LabelClaim, err) + } + + if role.Name == "" && claim.Name == "" { + log.Info("Skipping Pod, unable to find claim", "name", claimName) + return fmt.Errorf("unable to locate databaseclaim/dbroleclaim %q", claimName) + } + + secretName, class, err := getSecretNameClass(claim, role) + if err != nil { + return err + } + if class != p.class { + log.Info("Skipping Pod, class mismatch", "class", class, "expected", p.class) + return nil + } + + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + + if pod.Labels[LabelCheckProxy] == "enabled" && + pod.Annotations[AnnotationInjectedProxy] != "true" { + err = mutatePodProxy(ctx, pod, secretName, p.dbProxyImg) + if err == nil { + log.Info("mutated_pod_dbproxy") + } + } else if pod.Labels[LabelCheckExec] == "enabled" && + pod.Annotations[AnnotationInjectedExec] != "true" { + err = mutatePodExec(ctx, pod, secretName, p.dsnExecImg) + if err == nil { + log.Info("mutated_pod_dsnexec") + } + } else { + // No label found, skip + log.Info("Skipping Pod, no matching proxy or role label found") + return nil + } + + if err != nil { + log.Error(err, "failed to mutate pod") + } + + return err +} + +// getSecretNameClass returns the secret managed by db-controller and class for the claim +func getSecretNameClass(claim v1.DatabaseClaim, role v1.DbRoleClaim) (string, string, error) { + var secretName, className string + var claimName string + if role.Name != "" { + claimName = role.Name + secretName = role.Spec.SecretName + className = *role.Spec.Class + } else { + claimName = claim.Name + secretName = claim.Spec.SecretName + className = *claim.Spec.Class + } + + if secretName == "" { + return "", "", fmt.Errorf("claim %q does not have secret reference", claimName) + } + return secretName, className, nil +} diff --git a/internal/webhook/dsnexec.go b/internal/webhook/dsnexec.go new file mode 100644 index 00000000..62bbce45 --- /dev/null +++ b/internal/webhook/dsnexec.go @@ -0,0 +1,125 @@ +package webhook + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" +) + +var ( + MountPathExec = "/var/run/db-dsn" + VolumeNameExec = "db-dsn" + ContainerNameExec = "dsnexec" + + MountPathExecConfig = "/var/run/dsn-exec" + VolumeNameExecConfig = "dsnexec-config" +) + +func mutatePodExec(ctx context.Context, pod *corev1.Pod, secretName string, dsnExecImg string) error { + + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + + dsnExecConfigSecret := pod.Annotations["infoblox.com/dsnexec-config-secret"] + if dsnExecConfigSecret == "" { + dsnExecConfigSecret = pod.Labels[LabelConfigExec] + if dsnExecConfigSecret == "" { + return fmt.Errorf("%s label was not found", LabelConfigExec) + } + } + + // Process volume, check if existing + var foundVolume, foundCfgVolume bool + for _, v := range pod.Spec.Volumes { + if v.Name == VolumeNameExec { + foundVolume = true + } else if v.Name == VolumeNameExecConfig { + foundCfgVolume = true + } + } + + if !foundVolume { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: VolumeNameExec, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + }) + } + + if !foundCfgVolume { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: VolumeNameExecConfig, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: dsnExecConfigSecret, + }, + }, + }) + } + + var foundContainer bool + for _, c := range pod.Spec.Containers { + if c.Name == ContainerNameExec { + foundContainer = true + break + } + } + + if foundContainer { + pod.Annotations[AnnotationInjectedExec] = "true" + return nil + } + + pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{ + Name: ContainerNameExec, + Image: dsnExecImg, + ImagePullPolicy: corev1.PullIfNotPresent, + Args: []string{ + "run", + "--config-file", + fmt.Sprintf("%s/config.yaml", MountPathExecConfig), + }, + Env: []corev1.EnvVar{ + { + Name: "DBPROXY_CREDENTIAL", + Value: fmt.Sprintf("%s/%s", MountPathExec, SecretKey), + }, + }, + // Test connection to upstream database + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "/bin/sh", + "-c", + fmt.Sprintf("psql \"$(cat %s/%s)\" -c \"SELECT 1\"", MountPathExec, SecretKey), + }, + }, + }, + InitialDelaySeconds: 30, + PeriodSeconds: 15, + TimeoutSeconds: 5, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: VolumeNameExec, + MountPath: MountPathExec, + ReadOnly: true, + }, + { + Name: VolumeNameExecConfig, + MountPath: MountPathExecConfig, + ReadOnly: true, + }, + }, + }) + + pod.Annotations[AnnotationInjectedExec] = "true" + + return nil +} diff --git a/internal/webhook/dsnexec_test.go b/internal/webhook/dsnexec_test.go new file mode 100644 index 00000000..74dc80a8 --- /dev/null +++ b/internal/webhook/dsnexec_test.go @@ -0,0 +1,182 @@ +package webhook + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + + v1 "github.com/infobloxopen/db-controller/api/v1" +) + +var _ = Describe("dsnexec defaulting", func() { + BeforeEach(func() { + + By("create dependent resources") + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + resource := &v1.DatabaseClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-db", + Namespace: "default", + }, + Spec: v1.DatabaseClaimSpec{ + Class: ptr.To("default"), + SecretName: "test", + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + + }) + + AfterEach(func() { + resource := &v1.DatabaseClaim{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "default-db"}, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + + var secret corev1.Secret + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "test"}, &secret) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource core objects") + Expect(k8sClient.Delete(ctx, &secret)).To(Succeed()) + + var list corev1.PodList + Expect(k8sClient.List(ctx, &list)).ToNot(HaveOccurred()) + for _, item := range list.Items { + err := k8sClient.Delete(ctx, &item) + Expect(err).ToNot(HaveOccurred()) + } + + }) + + // TODO: change to table driven tests + It("should successfully mutating a pod", func() { + By("Check pod is mutated") + + name := "annotation-enabled" + Expect(k8sClient.Create(ctx, makePodExec(name, "default-db"))).NotTo(HaveOccurred()) + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) + Expect(err).NotTo(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjectedExec, "true")) + + }) + + It("should successfully skip mutation", func() { + By("Check pod is mutated") + + name := "annotation-disabled" + + Expect(k8sClient.Create(ctx, makePodExec(name, ""))).NotTo(HaveOccurred()) + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) + Expect(err).NotTo(HaveOccurred()) + Expect(pod.Annotations).NotTo(HaveKeyWithValue(AnnotationInjectedExec, "true")) + + name = "annotation-false" + + Expect(k8sClient.Create(ctx, makePodExec(name, ""))).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) + Expect(err).NotTo(HaveOccurred()) + Expect(pod.Annotations).NotTo(HaveKeyWithValue(AnnotationInjectedExec, "true")) + Expect(pod.Spec.Volumes).To(HaveLen(0)) + Expect(pod.Spec.Containers).To(HaveLen(1)) + + }) + + It("check initial volume and sidecar pod mutation", func() { + + name := "annotation-enabled" + Expect(k8sClient.Create(ctx, makePodExec(name, "default-db"))).NotTo(HaveOccurred()) + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) + Expect(err).NotTo(HaveOccurred()) + Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjectedExec, "true")) + By("Check secret volume") + Expect(pod.Spec.Volumes).To(HaveLen(2)) + Expect(pod.Spec.Volumes[0].Name).To(Equal(VolumeNameExec)) + Expect(pod.Spec.Volumes[0].VolumeSource.Secret.SecretName).To(Equal("test")) + Expect(pod.Spec.Volumes[0].VolumeSource.Secret.Optional).To(BeNil()) + Expect(pod.Spec.Volumes[1].Name).To(Equal(VolumeNameExecConfig)) + Expect(pod.Spec.Volumes[1].VolumeSource.Secret.SecretName).To(Equal("dsnexec-config-secret")) + Expect(pod.Spec.Volumes[1].VolumeSource.Secret.Optional).To(BeNil()) + + By("Check sidecar pod is injected") + sidecar := pod.Spec.Containers[len(pod.Spec.Containers)-1] + Expect(sidecar.Image).To(Equal(sidecarImageExec)) + Expect(sidecar.VolumeMounts).To(HaveLen(2)) + Expect(sidecar.VolumeMounts[0].Name).To(Equal(VolumeNameExec)) + Expect(sidecar.VolumeMounts[0].MountPath).To(Equal(MountPathExec)) + Expect(sidecar.VolumeMounts[0].ReadOnly).To(BeTrue()) + }) + + It("pre-mutated pods are not re-mutated", func() { + + name := "annotation-enabled" + Expect(k8sClient.Create(ctx, makeMutatedPodExec(name, "default-db", "test"))).NotTo(HaveOccurred()) + pod := &corev1.Pod{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: name}, pod) + Expect(err).NotTo(HaveOccurred()) + By("Check pod has one set of volumes and sidecars") + Expect(pod.Annotations).To(HaveKeyWithValue(AnnotationInjectedExec, "true")) + Expect(pod.Spec.Volumes).To(HaveLen(2)) + Expect(pod.Spec.Containers).To(HaveLen(2)) + }) + +}) + +func makeMutatedPodExec(name, claimName, secretName string) *corev1.Pod { + pod := makePodExec(name, claimName) + Expect(mutatePodExec(context.TODO(), pod, secretName, sidecarImageExec)).To(Succeed()) + return pod + +} + +func makePodExec(name, claimName string) *corev1.Pod { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "test", + Image: "test", + }, + }, + }, + } + + switch name { + case "annotation-enabled": + pod.Labels = map[string]string{ + LabelCheckExec: "enabled", + LabelClaim: claimName, + LabelConfigExec: "dsnexec-config-secret", + } + case "annotation-disabled": + pod.Labels = map[string]string{} + case "annotation-false": + pod.Labels = map[string]string{ + LabelCheckExec: "disabled", + } + } + + return pod +} diff --git a/internal/webhook/suite_test.go b/internal/webhook/suite_test.go index 1fe10ea4..5993dabe 100644 --- a/internal/webhook/suite_test.go +++ b/internal/webhook/suite_test.go @@ -37,7 +37,8 @@ var k8sClient client.Client var testEnv *envtest.Environment var ctx context.Context var cancel context.CancelFunc -var sidecarImage = "dbproxy:latest" +var sidecarImageProxy = "dbproxy:latest" +var sidecarImageExec = "dsnexec:latest" func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -99,7 +100,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = SetupWebhookWithManager(mgr, SetupConfig{ - DBProxyImg: sidecarImage, + DBProxyImg: sidecarImageProxy, + DSNExecImg: sidecarImageExec, Namespace: "default", Class: "default", }) diff --git a/webhook/webhook-dsnexec.go b/webhook/webhook-dsnexec.go deleted file mode 100644 index e093e6a4..00000000 --- a/webhook/webhook-dsnexec.go +++ /dev/null @@ -1,120 +0,0 @@ -package webhook - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "os" - "strconv" - - corev1 "k8s.io/api/core/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -// DebugLevel is used to set V level to 1 as suggested by official docs -// https://github.com/kubernetes-sigs/controller-runtime/blob/main/TMP-LOGGING.md -const debugLevel = 1 - -type DsnExecInjector struct { - Name string - Client client.Client - Decoder admission.Decoder - DsnExecSidecarConfig *Config -} - -var ( - dsnexecLog = ctrl.Log.WithName("dsnexec-controller") - sidecarImageForDsnExec = os.Getenv("DSNEXEC_IMAGE") -) - -func dsnExecSideCarInjectionRequired(pod *corev1.Pod) (bool, string, string) { - remoteDbSecretName, ok := pod.Annotations["infoblox.com/remote-db-dsn-secret"] - if !ok { - return false, "", "" - } - - dsnExecConfigSecret, ok := pod.Annotations["infoblox.com/dsnexec-config-secret"] - if !ok { - return false, "", "" - } - - alreadyInjected, err := strconv.ParseBool(pod.Annotations["infoblox.com/dsnexec-injected"]) - - if err == nil && alreadyInjected { - dsnexecLog.V(debugLevel).Info("DsnExec sidecar already injected: ", pod.Name, pod.Annotations) - return false, remoteDbSecretName, dsnExecConfigSecret - } - - dsnexecLog.V(debugLevel).Info("DsnExec sidecar Injection required: ", pod.Name, pod.Annotations) - - return true, remoteDbSecretName, dsnExecConfigSecret -} - -type Config struct { - Containers []corev1.Container `json:"containers"` - Volumes []corev1.Volume `json:"volumes"` -} - -func ParseConfig(configFile string) (*Config, error) { - data, err := ioutil.ReadFile(configFile) - if err != nil { - return nil, err - } - - var cfg Config - if err := json.Unmarshal(data, &cfg); err != nil { - return nil, err - } - - return &cfg, nil -} - -// DsnExecInjector adds an annotation to every incoming pods. -func (dbpi *DsnExecInjector) Handle(ctx context.Context, req admission.Request) admission.Response { - pod := &corev1.Pod{} - - decoder := admission.NewDecoder(dbpi.Client.Scheme()) - err := decoder.Decode(req, pod) - - if err != nil { - dsnexecLog.Error(err, "Sdecar-Injector: cannot decode") - return admission.Errored(http.StatusBadRequest, err) - } - - if pod.Annotations == nil { - pod.Annotations = map[string]string{} - } - - shoudInjectDsnExec, remoteDbSecretName, dsnExecConfigSecret := dsnExecSideCarInjectionRequired(pod) - - if shoudInjectDsnExec { - dsnexecLog.V(debugLevel).Info("Injecting sidecar...") - - dbpi.DsnExecSidecarConfig.Containers[0].Image = sidecarImageForDsnExec - dbpi.DsnExecSidecarConfig.Volumes[0].Secret.SecretName = remoteDbSecretName - dbpi.DsnExecSidecarConfig.Volumes[1].Secret.SecretName = dsnExecConfigSecret - - pod.Spec.Volumes = append(pod.Spec.Volumes, dbpi.DsnExecSidecarConfig.Volumes...) - pod.Spec.Containers = append(pod.Spec.Containers, dbpi.DsnExecSidecarConfig.Containers...) - - pod.Annotations["infoblox.com/dsnexec-injected"] = "true" - shareProcessNamespace := true - pod.Spec.ShareProcessNamespace = &shareProcessNamespace - - dsnexecLog.V(debugLevel).Info("sidecar ontainer for ", dbpi.Name, " injected.", pod.Name, pod.APIVersion) - - } else { - dsnexecLog.V(debugLevel).Info("dsnexec sidecar not needed.", pod.Name, pod.APIVersion) - } - marshaledPod, err := json.Marshal(pod) - - if err != nil { - dsnexecLog.Error(err, "dsnexec sidecar injection: cannot marshal") - return admission.Errored(http.StatusInternalServerError, err) - } - - return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) -}