diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index 30ca8b44d..ef3541183 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.9.5 +version: v0.9.6 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.9.5" +appVersion: "v0.9.6" diff --git a/helm-charts/secrets-operator/templates/deployment.yaml b/helm-charts/secrets-operator/templates/deployment.yaml index ca7b16401..95cf36801 100644 --- a/helm-charts/secrets-operator/templates/deployment.yaml +++ b/helm-charts/secrets-operator/templates/deployment.yaml @@ -22,7 +22,11 @@ spec: kubectl.kubernetes.io/default-container: manager spec: containers: - - args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + - args: + {{- toYaml .Values.controllerManager.manager.args | nindent 8 }} + {{- if and .Values.scopedNamespace .Values.scopedRBAC }} + - --namespace={{ .Values.scopedNamespace }} + {{- end }} command: - /manager env: diff --git a/helm-charts/secrets-operator/templates/manager-rbac.yaml b/helm-charts/secrets-operator/templates/manager-rbac.yaml index 9cd79264a..27f57e1a4 100644 --- a/helm-charts/secrets-operator/templates/manager-rbac.yaml +++ b/helm-charts/secrets-operator/templates/manager-rbac.yaml @@ -48,7 +48,9 @@ rules: - apiGroups: - apps resources: + - daemonsets - deployments + - statefulsets verbs: - get - list diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index 896c93f60..0894aa07a 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -12,7 +12,7 @@ controllerManager: readOnlyRootFilesystem: true image: repository: infisical/kubernetes-operator - tag: v0.9.5 + tag: v0.9.6 resources: limits: cpu: 500m diff --git a/k8-operator/Makefile b/k8-operator/Makefile index 463b1537c..ec1690744 100644 --- a/k8-operator/Makefile +++ b/k8-operator/Makefile @@ -37,7 +37,7 @@ legacy-helm: manifests kustomize helmify helm: manifests kustomize helmify cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - ./scripts/generate-helm.sh + ./scripts/generate-helm.sh ${VERSION} cd config/manager && $(KUSTOMIZE) edit set image controller=controller:latest # reset back ## Yaml for Kubectl @@ -88,7 +88,7 @@ test: manifests generate fmt vet setup-envtest ## Run tests. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # CertManager is installed by default; skip with: # - CERT_MANAGER_INSTALL_SKIP=true -KIND_CLUSTER ?= k8-operator-test-e2e +KIND_CLUSTER ?= infisical-operator-test-e2e .PHONY: setup-test-e2e setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist @@ -157,10 +157,10 @@ PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - - $(CONTAINER_TOOL) buildx create --name k8-operator-builder - $(CONTAINER_TOOL) buildx use k8-operator-builder + - $(CONTAINER_TOOL) buildx create --name infisical-operator-builder + $(CONTAINER_TOOL) buildx use infisical-operator-builder - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - - $(CONTAINER_TOOL) buildx rm k8-operator-builder + - $(CONTAINER_TOOL) buildx rm infisical-operator-builder rm Dockerfile.cross .PHONY: build-installer diff --git a/k8-operator/cmd/main.go b/k8-operator/cmd/main.go index 1b71e0024..40142a446 100644 --- a/k8-operator/cmd/main.go +++ b/k8-operator/cmd/main.go @@ -19,6 +19,7 @@ package main import ( "crypto/tls" "flag" + "fmt" "os" "path/filepath" @@ -30,6 +31,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/certwatcher" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -63,7 +65,10 @@ func main() { var probeAddr string var secureMetrics bool var enableHTTP2 bool + var namespace string + var tlsOpts []func(*tls.Config) + flag.StringVar(&namespace, "namespace", "", "Watch InfisicalSecrets scoped in the provided namespace only") flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -178,7 +183,7 @@ func main() { }) } - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + managerOptions := ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, @@ -196,32 +201,51 @@ func main() { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, - }) + } + + // Only set cache options if we're namespace-scoped + if namespace != "" { + managerOptions.Cache = cache.Options{ + Scheme: scheme, + DefaultNamespaces: map[string]cache.Config{ + namespace: {}, // whichever namespace the operator is running in + }, + } + ctrl.Log.Info(fmt.Sprintf("Watching CRDs in [namespace=%s]", namespace)) + } + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), managerOptions) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } if err := (&controller.InfisicalSecretReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - BaseLogger: ctrl.Log, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BaseLogger: ctrl.Log, + Namespace: namespace, + IsNamespaceScoped: namespace != "", }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "InfisicalSecret") os.Exit(1) } if err := (&controller.InfisicalPushSecretReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - BaseLogger: ctrl.Log, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + IsNamespaceScoped: namespace != "", + Namespace: namespace, + BaseLogger: ctrl.Log, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "InfisicalPushSecret") os.Exit(1) } if err := (&controller.InfisicalDynamicSecretReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - BaseLogger: ctrl.Log, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BaseLogger: ctrl.Log, + IsNamespaceScoped: namespace != "", + Namespace: namespace, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "InfisicalDynamicSecret") os.Exit(1) diff --git a/k8-operator/config/default/kustomization.yaml b/k8-operator/config/default/kustomization.yaml index 8eda77014..87497e05a 100644 --- a/k8-operator/config/default/kustomization.yaml +++ b/k8-operator/config/default/kustomization.yaml @@ -1,12 +1,12 @@ # Adds namespace to all resources. -namespace: k8-operator-system +namespace: infisical-operator-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. -namePrefix: k8-operator- +namePrefix: infisical-operator- # Labels to add to all resources and selectors. #labels: @@ -15,220 +15,12 @@ namePrefix: k8-operator- # someName: someValue resources: -- ../crd -- ../rbac -- ../manager -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- ../webhook -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager -# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. -#- ../prometheus -# [METRICS] Expose the controller manager metrics service. -- metrics_service.yaml -# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. -# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. -# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will -# be able to communicate with the Webhook Server. -#- ../network-policy + - ../crd + - ../rbac + - ../manager + - metrics_service.yaml -# Uncomment the patches line if you enable Metrics patches: -# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. -# More info: https://book.kubebuilder.io/reference/metrics -- path: manager_metrics_patch.yaml - target: - kind: Deployment - -# Uncomment the patches line if you enable Metrics and CertManager -# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. -# This patch will protect the metrics with certManager self-signed certs. -#- path: cert_metrics_manager_patch.yaml -# target: -# kind: Deployment - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- path: manager_webhook_patch.yaml -# target: -# kind: Deployment - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -# Uncomment the following replacements to add the cert-manager CA injection annotations -#replacements: -# - source: # Uncomment the following block to enable certificates for metrics -# kind: Service -# version: v1 -# name: controller-manager-metrics-service -# fieldPath: metadata.name -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: metrics-certs -# fieldPaths: -# - spec.dnsNames.0 -# - spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor -# kind: ServiceMonitor -# group: monitoring.coreos.com -# version: v1 -# name: controller-manager-metrics-monitor -# fieldPaths: -# - spec.endpoints.0.tlsConfig.serverName -# options: -# delimiter: '.' -# index: 0 -# create: true - -# - source: -# kind: Service -# version: v1 -# name: controller-manager-metrics-service -# fieldPath: metadata.namespace -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: metrics-certs -# fieldPaths: -# - spec.dnsNames.0 -# - spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true -# - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor -# kind: ServiceMonitor -# group: monitoring.coreos.com -# version: v1 -# name: controller-manager-metrics-monitor -# fieldPaths: -# - spec.endpoints.0.tlsConfig.serverName -# options: -# delimiter: '.' -# index: 1 -# create: true - -# - source: # Uncomment the following block if you have any webhook -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.name # Name of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 0 -# create: true -# - source: -# kind: Service -# version: v1 -# name: webhook-service -# fieldPath: .metadata.namespace # Namespace of the service -# targets: -# - select: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPaths: -# - .spec.dnsNames.0 -# - .spec.dnsNames.1 -# options: -# delimiter: '.' -# index: 1 -# create: true - -# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # This name should match the one in certificate.yaml -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: ValidatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true - -# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 0 -# create: true -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: -# - select: -# kind: MutatingWebhookConfiguration -# fieldPaths: -# - .metadata.annotations.[cert-manager.io/inject-ca-from] -# options: -# delimiter: '/' -# index: 1 -# create: true - -# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.namespace # Namespace of the certificate CR -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# +kubebuilder:scaffold:crdkustomizecainjectionns -# - source: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert -# fieldPath: .metadata.name -# targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. -# +kubebuilder:scaffold:crdkustomizecainjectionname + - path: manager_metrics_patch.yaml + target: + kind: Deployment diff --git a/k8-operator/config/rbac/role.yaml b/k8-operator/config/rbac/role.yaml index 67216ba4f..97151dd0b 100644 --- a/k8-operator/config/rbac/role.yaml +++ b/k8-operator/config/rbac/role.yaml @@ -40,7 +40,9 @@ rules: - apiGroups: - apps resources: + - daemonsets - deployments + - statefulsets verbs: - get - list diff --git a/k8-operator/internal/controller/infisicaldynamicsecret_controller.go b/k8-operator/internal/controller/infisicaldynamicsecret_controller.go index adbc6b03c..f63723c7d 100644 --- a/k8-operator/internal/controller/infisicaldynamicsecret_controller.go +++ b/k8-operator/internal/controller/infisicaldynamicsecret_controller.go @@ -42,9 +42,11 @@ import ( // InfisicalDynamicSecretReconciler reconciles a InfisicalDynamicSecret object type InfisicalDynamicSecretReconciler struct { client.Client - BaseLogger logr.Logger - Scheme *runtime.Scheme - Random *rand.Rand + BaseLogger logr.Logger + Scheme *runtime.Scheme + Random *rand.Rand + Namespace string + IsNamespaceScoped bool } var infisicalDynamicSecretsResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) @@ -106,7 +108,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } // Initialize the business logic handler - handler := infisicaldynamicsecret.NewInfisicalDynamicSecretHandler(r.Client, r.Scheme) + handler := infisicaldynamicsecret.NewInfisicalDynamicSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped) err := handler.HandleLeaseRevocation(ctx, logger, &infisicalDynamicSecretCRD, infisicalDynamicSecretsResourceVariablesMap) @@ -126,7 +128,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } // Get modified/default config - infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client) + infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client, r.IsNamespaceScoped) if err != nil { logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ @@ -135,7 +137,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } // Initialize the business logic handler - handler := infisicaldynamicsecret.NewInfisicalDynamicSecretHandler(r.Client, r.Scheme) + handler := infisicaldynamicsecret.NewInfisicalDynamicSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped) // Setup API configuration through business logic err = handler.SetupAPIConfig(infisicalDynamicSecretCRD, infisicalConfig) @@ -169,7 +171,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct }, nil } - numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference) + numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference, r.IsNamespaceScoped) handler.SetReconcileAutoRedeploymentConditionStatus(ctx, logger, &infisicalDynamicSecretCRD, numDeployments, err) if err != nil { diff --git a/k8-operator/internal/controller/infisicalpushsecret_controller.go b/k8-operator/internal/controller/infisicalpushsecret_controller.go index dc5bfad8b..e8665b5d9 100644 --- a/k8-operator/internal/controller/infisicalpushsecret_controller.go +++ b/k8-operator/internal/controller/infisicalpushsecret_controller.go @@ -45,9 +45,10 @@ import ( // InfisicalPushSecretReconciler reconciles a InfisicalPushSecretSecret object type InfisicalPushSecretReconciler struct { client.Client - IsNamespaceScoped bool BaseLogger logr.Logger Scheme *runtime.Scheme + IsNamespaceScoped bool + Namespace string } var infisicalPushSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) @@ -158,7 +159,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. } // Get modified/default config - infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client) + infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client, r.IsNamespaceScoped) if err != nil { if requeueTime != 0 { logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime)) diff --git a/k8-operator/internal/controller/infisicalsecret_controller.go b/k8-operator/internal/controller/infisicalsecret_controller.go index 3cea12842..d5d9c5d39 100644 --- a/k8-operator/internal/controller/infisicalsecret_controller.go +++ b/k8-operator/internal/controller/infisicalsecret_controller.go @@ -41,8 +41,10 @@ import ( // InfisicalSecretReconciler reconciles a InfisicalSecret object type InfisicalSecretReconciler struct { client.Client - BaseLogger logr.Logger - Scheme *runtime.Scheme + BaseLogger logr.Logger + Scheme *runtime.Scheme + Namespace string + IsNamespaceScoped bool } var infisicalSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) @@ -51,9 +53,16 @@ func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName) } -// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets;statefulsets,verbs=list;watch;get;update +//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch +//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list +//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create +//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -138,7 +147,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // Get modified/default config - infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client) + infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client, r.IsNamespaceScoped) if err != nil { logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ @@ -147,7 +156,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // Initialize the business logic handler - handler := infisicalsecret.NewInfisicalSecretHandler(r.Client, r.Scheme) + handler := infisicalsecret.NewInfisicalSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped) // Setup API configuration through business logic err = handler.SetupAPIConfig(infisicalSecretCRD, infisicalConfig) @@ -177,7 +186,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ }, nil } - numDeployments, err := controllerhelpers.ReconcileDeploymentsWithMultipleManagedSecrets(ctx, r.Client, logger, managedKubeSecretReferences) + numDeployments, err := controllerhelpers.ReconcileDeploymentsWithMultipleManagedSecrets(ctx, r.Client, logger, managedKubeSecretReferences, r.IsNamespaceScoped) handler.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err) if err != nil { diff --git a/k8-operator/internal/controllerhelpers/controllerhelpers.go b/k8-operator/internal/controllerhelpers/controllerhelpers.go index 0149d90a0..d33097c37 100644 --- a/k8-operator/internal/controllerhelpers/controllerhelpers.go +++ b/k8-operator/internal/controllerhelpers/controllerhelpers.go @@ -7,6 +7,7 @@ import ( "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/internal/constants" + "github.com/Infisical/infisical/k8-operator/internal/util" "github.com/go-logr/logr" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -19,7 +20,7 @@ import ( const DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX = "secrets.infisical.com/managed-secret" const AUTO_RELOAD_DEPLOYMENT_ANNOTATION = "secrets.infisical.com/auto-reload" // needs to be set to true for a deployment to start auto redeploying -func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecret v1alpha1.ManagedKubeSecretConfig) (int, error) { +func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecret v1alpha1.ManagedKubeSecretConfig, isNamespaceScoped bool) (int, error) { listOfDeployments := &v1.DeploymentList{} err := client.List(ctx, listOfDeployments, &controllerClient.ListOptions{Namespace: managedSecret.SecretNamespace}) @@ -47,6 +48,10 @@ func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controll managedKubeSecret := &corev1.Secret{} err = client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret) if err != nil { + if util.IsNamespaceScopedError(err, isNamespaceScoped) { + return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the secret is in the same namespace as the operator. [err=%v]", err) + } + return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err) } @@ -100,9 +105,9 @@ func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controll return 0, nil } -func ReconcileDeploymentsWithMultipleManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecrets []v1alpha1.ManagedKubeSecretConfig) (int, error) { +func ReconcileDeploymentsWithMultipleManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecrets []v1alpha1.ManagedKubeSecretConfig, isNamespaceScoped bool) (int, error) { for _, managedSecret := range managedSecrets { - _, err := ReconcileDeploymentsWithManagedSecrets(ctx, client, logger, managedSecret) + _, err := ReconcileDeploymentsWithManagedSecrets(ctx, client, logger, managedSecret, isNamespaceScoped) if err != nil { logger.Error(err, fmt.Sprintf("unable to reconcile deployments with managed secret [name=%v]", managedSecret.SecretName)) return 0, err @@ -259,11 +264,17 @@ func ReconcileStatefulSet(ctx context.Context, client controllerClient.Client, l return nil } -func GetInfisicalConfigMap(ctx context.Context, client client.Client) (configMap map[string]string, errToReturn error) { +func GetInfisicalConfigMap(ctx context.Context, client client.Client, isNamespaceScoped bool) (configMap map[string]string, errToReturn error) { // default key values defaultConfigMapData := make(map[string]string) defaultConfigMapData["hostAPI"] = constants.INFISICAL_DOMAIN + // this will never work if we're namespace scoped, because the operator can't read outside of its namespace by our current RBAC rules. + // This is how it has always worked, but the error has been masked as 'not found' in V3 kubebuilder. + if isNamespaceScoped { + return defaultConfigMapData, nil + } + kubeConfigMap := &corev1.ConfigMap{} err := client.Get(ctx, types.NamespacedName{ Namespace: constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, diff --git a/k8-operator/internal/controllerutil/util.go b/k8-operator/internal/controllerutil/util.go deleted file mode 100644 index 67e0cdbe4..000000000 --- a/k8-operator/internal/controllerutil/util.go +++ /dev/null @@ -1,45 +0,0 @@ -package controllerhelpers - -import ( - "context" - "fmt" - - "github.com/Infisical/infisical/k8-operator/internal/constants" - corev1 "k8s.io/api/core/v1" - k8Errors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func GetInfisicalConfigMap(ctx context.Context, client client.Client) (configMap map[string]string, errToReturn error) { - // default key values - defaultConfigMapData := make(map[string]string) - defaultConfigMapData["hostAPI"] = constants.INFISICAL_DOMAIN - - kubeConfigMap := &corev1.ConfigMap{} - err := client.Get(ctx, types.NamespacedName{ - Namespace: constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, - Name: constants.OPERATOR_SETTINGS_CONFIGMAP_NAME, - }, kubeConfigMap) - - if err != nil { - if k8Errors.IsNotFound(err) { - kubeConfigMap = nil - } else { - return nil, fmt.Errorf("GetConfigMapByNamespacedName: unable to fetch config map in [namespacedName=%s] [err=%s]", constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, err) - } - } - - if kubeConfigMap == nil { - return defaultConfigMapData, nil - } else { - for key, value := range defaultConfigMapData { - _, exists := kubeConfigMap.Data[key] - if !exists { - kubeConfigMap.Data[key] = value - } - } - - return kubeConfigMap.Data, nil - } -} diff --git a/k8-operator/internal/services/infisicaldynamicsecret/handler.go b/k8-operator/internal/services/infisicaldynamicsecret/handler.go index 71ce591d5..1749cf5c0 100644 --- a/k8-operator/internal/services/infisicaldynamicsecret/handler.go +++ b/k8-operator/internal/services/infisicaldynamicsecret/handler.go @@ -19,15 +19,17 @@ import ( type InfisicalDynamicSecretHandler struct { client.Client - Scheme *runtime.Scheme - Random *rand.Rand + Scheme *runtime.Scheme + Random *rand.Rand + IsNamespaceScoped bool } -func NewInfisicalDynamicSecretHandler(client client.Client, scheme *runtime.Scheme) *InfisicalDynamicSecretHandler { +func NewInfisicalDynamicSecretHandler(client client.Client, scheme *runtime.Scheme, isNamespaceScoped bool) *InfisicalDynamicSecretHandler { return &InfisicalDynamicSecretHandler{ - Client: client, - Scheme: scheme, - Random: rand.New(rand.NewSource(time.Now().UnixNano())), + Client: client, + Scheme: scheme, + Random: rand.New(rand.NewSource(time.Now().UnixNano())), + IsNamespaceScoped: isNamespaceScoped, } } @@ -51,6 +53,10 @@ func (h *InfisicalDynamicSecretHandler) getInfisicalCaCertificateFromKubeSecret( return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err) } + if util.IsNamespaceScopedError(err, h.IsNamespaceScoped) { + return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err) + } + if err != nil { return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err) } @@ -75,36 +81,40 @@ func (h *InfisicalDynamicSecretHandler) HandleCACertificate(ctx context.Context, func (h *InfisicalDynamicSecretHandler) ReconcileInfisicalDynamicSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) (time.Duration, error) { reconciler := &InfisicalDynamicSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, - Random: h.Random, + Client: h.Client, + Scheme: h.Scheme, + Random: h.Random, + IsNamespaceScoped: h.IsNamespaceScoped, } return reconciler.ReconcileInfisicalDynamicSecret(ctx, logger, infisicalDynamicSecret, resourceVariablesMap) } func (h *InfisicalDynamicSecretHandler) HandleLeaseRevocation(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) error { reconciler := &InfisicalDynamicSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, - Random: h.Random, + Client: h.Client, + Scheme: h.Scheme, + Random: h.Random, + IsNamespaceScoped: h.IsNamespaceScoped, } return reconciler.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecret, resourceVariablesMap) } func (h *InfisicalDynamicSecretHandler) SetReconcileConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { reconciler := &InfisicalDynamicSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, - Random: h.Random, + Client: h.Client, + Scheme: h.Scheme, + Random: h.Random, + IsNamespaceScoped: h.IsNamespaceScoped, } reconciler.SetReconcileConditionStatus(ctx, logger, infisicalDynamicSecret, errorToConditionOn) } func (h *InfisicalDynamicSecretHandler) SetReconcileAutoRedeploymentConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, numDeployments int, errorToConditionOn error) { reconciler := &InfisicalDynamicSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, - Random: h.Random, + Client: h.Client, + Scheme: h.Scheme, + Random: h.Random, + IsNamespaceScoped: h.IsNamespaceScoped, } reconciler.SetReconcileAutoRedeploymentConditionStatus(ctx, logger, infisicalDynamicSecret, numDeployments, errorToConditionOn) } diff --git a/k8-operator/internal/services/infisicaldynamicsecret/reconciler.go b/k8-operator/internal/services/infisicaldynamicsecret/reconciler.go index 6525dc433..e763e9308 100644 --- a/k8-operator/internal/services/infisicaldynamicsecret/reconciler.go +++ b/k8-operator/internal/services/infisicaldynamicsecret/reconciler.go @@ -2,7 +2,6 @@ package infisicaldynamicsecret import ( "context" - "errors" "fmt" "math/rand" "strings" @@ -27,8 +26,9 @@ import ( type InfisicalDynamicSecretReconciler struct { client.Client - Scheme *runtime.Scheme - Random *rand.Rand + Scheme *runtime.Scheme + Random *rand.Rand + IsNamespaceScoped bool } func (r *InfisicalDynamicSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret, versionAnnotationValue string) error { @@ -85,36 +85,6 @@ func (r *InfisicalDynamicSecretReconciler) createInfisicalManagedKubeSecret(ctx return nil } -func (r *InfisicalDynamicSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalDynamicSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) { - authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){ - util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth, - util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth, - util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth, - util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth, - util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth, - util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth, - util.AuthStrategy.LDAP_MACHINE_IDENTITY: util.HandleLdapAuth, - } - - for authStrategy, authHandler := range authStrategies { - authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{ - Secret: infisicalSecret, - Type: util.SecretCrd.INFISICAL_DYNAMIC_SECRET, - }, infisicalClient) - - if err == nil { - return authDetails, nil - } - - if !errors.Is(err, util.ErrAuthNotApplicable) { - return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) - } - } - - return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided") - -} - func (r *InfisicalDynamicSecretReconciler) getResourceVariables(infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) util.ResourceVariables { var resourceVariables util.ResourceVariables @@ -254,7 +224,10 @@ func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Con infisicalClient := resourceVariables.InfisicalClient logger.Info("Authenticating for lease revocation") - authDetails, err := r.handleAuthentication(ctx, *infisicalDynamicSecret, infisicalClient) + authDetails, err := util.HandleAuthentication(ctx, util.SecretAuthInput{ + Secret: *infisicalDynamicSecret, + Type: util.SecretCrd.INFISICAL_DYNAMIC_SECRET, + }, r.Client, infisicalClient, r.IsNamespaceScoped) if err != nil { return fmt.Errorf("unable to authenticate for lease revocation [err=%s]", err) @@ -290,6 +263,9 @@ func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Con }) if err != nil { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err) + } return fmt.Errorf("unable to fetch destination secret [err=%s]", err) } @@ -318,7 +294,10 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") - authDetails, err = r.handleAuthentication(ctx, *infisicalDynamicSecret, infisicalClient) + authDetails, err = util.HandleAuthentication(ctx, util.SecretAuthInput{ + Secret: *infisicalDynamicSecret, + Type: util.SecretCrd.INFISICAL_DYNAMIC_SECRET, + }, r.Client, infisicalClient, r.IsNamespaceScoped) if err != nil { return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err) @@ -337,6 +316,9 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c }) if err != nil { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return nextReconcile, fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err) + } if k8Errors.IsNotFound(err) { annotationValue := "" @@ -352,6 +334,9 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c }) if err != nil { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return nextReconcile, fmt.Errorf("unable to fetch Kubernetes destination secret after creation. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err) + } return nextReconcile, fmt.Errorf("unable to fetch destination secret after creation [err=%s]", err) } diff --git a/k8-operator/internal/services/infisicalpushsecret/handler.go b/k8-operator/internal/services/infisicalpushsecret/handler.go index abe0266d6..476b2bda2 100644 --- a/k8-operator/internal/services/infisicalpushsecret/handler.go +++ b/k8-operator/internal/services/infisicalpushsecret/handler.go @@ -49,6 +49,10 @@ func (h *InfisicalPushSecretHandler) getInfisicalCaCertificateFromKubeSecret(ctx return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err) } + if util.IsNamespaceScopedError(err, h.IsNamespaceScoped) { + return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err) + } + if err != nil { return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err) } diff --git a/k8-operator/internal/services/infisicalpushsecret/reconciler.go b/k8-operator/internal/services/infisicalpushsecret/reconciler.go index 6c4de31c8..cdfc326bb 100644 --- a/k8-operator/internal/services/infisicalpushsecret/reconciler.go +++ b/k8-operator/internal/services/infisicalpushsecret/reconciler.go @@ -3,7 +3,6 @@ package infisicalpushsecret import ( "bytes" "context" - "errors" "fmt" "strings" tpl "text/template" @@ -30,36 +29,6 @@ type InfisicalPushSecretReconciler struct { IsNamespaceScoped bool } -func (r *InfisicalPushSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalPushSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) { - authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){ - util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth, - util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth, - util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth, - util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth, - util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth, - util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth, - util.AuthStrategy.LDAP_MACHINE_IDENTITY: util.HandleLdapAuth, - } - - for authStrategy, authHandler := range authStrategies { - authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{ - Secret: infisicalSecret, - Type: util.SecretCrd.INFISICAL_PUSH_SECRET, - }, infisicalClient) - - if err == nil { - return authDetails, nil - } - - if !errors.Is(err, util.ErrAuthNotApplicable) { - return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) - } - } - - return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided") - -} - func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) util.ResourceVariables { var resourceVariables util.ResourceVariables @@ -192,7 +161,10 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") - authDetails, err = r.handleAuthentication(ctx, *infisicalPushSecret, infisicalClient) + authDetails, err = util.HandleAuthentication(ctx, util.SecretAuthInput{ + Secret: *infisicalPushSecret, + Type: util.SecretCrd.INFISICAL_PUSH_SECRET, + }, r.Client, infisicalClient, r.IsNamespaceScoped) r.SetAuthenticatedStatusCondition(ctx, infisicalPushSecret, err) if err != nil { @@ -215,6 +187,10 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context }) if err != nil { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err) + } + return fmt.Errorf("unable to fetch kube secret [err=%s]", err) } @@ -539,7 +515,10 @@ func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") - authDetails, err = r.handleAuthentication(ctx, *infisicalPushSecret, infisicalClient) + authDetails, err = util.HandleAuthentication(ctx, util.SecretAuthInput{ + Secret: *infisicalPushSecret, + Type: util.SecretCrd.INFISICAL_PUSH_SECRET, + }, r.Client, infisicalClient, r.IsNamespaceScoped) r.SetAuthenticatedStatusCondition(ctx, infisicalPushSecret, err) if err != nil { diff --git a/k8-operator/internal/services/infisicalsecret/handler.go b/k8-operator/internal/services/infisicalsecret/handler.go index ec3b50671..5657d701d 100644 --- a/k8-operator/internal/services/infisicalsecret/handler.go +++ b/k8-operator/internal/services/infisicalsecret/handler.go @@ -17,13 +17,15 @@ import ( type InfisicalSecretHandler struct { client.Client - Scheme *runtime.Scheme + Scheme *runtime.Scheme + IsNamespaceScoped bool } -func NewInfisicalSecretHandler(client client.Client, scheme *runtime.Scheme) *InfisicalSecretHandler { +func NewInfisicalSecretHandler(client client.Client, scheme *runtime.Scheme, isNamespaceScoped bool) *InfisicalSecretHandler { return &InfisicalSecretHandler{ - Client: client, - Scheme: scheme, + Client: client, + Scheme: scheme, + IsNamespaceScoped: isNamespaceScoped, } } @@ -48,6 +50,9 @@ func (h *InfisicalSecretHandler) getInfisicalCaCertificateFromKubeSecret(ctx con } if err != nil { + if util.IsNamespaceScopedError(err, h.IsNamespaceScoped) { + return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err) + } return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err) } @@ -71,24 +76,27 @@ func (h *InfisicalSecretHandler) HandleCACertificate(ctx context.Context, infisi func (h *InfisicalSecretHandler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig, managedKubeConfigMapReferences []v1alpha1.ManagedKubeConfigMapConfig, resourceVariablesMap map[string]util.ResourceVariables) (int, error) { reconciler := &InfisicalSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, + Client: h.Client, + Scheme: h.Scheme, + IsNamespaceScoped: h.IsNamespaceScoped, } return reconciler.ReconcileInfisicalSecret(ctx, logger, infisicalSecret, managedKubeSecretReferences, managedKubeConfigMapReferences, resourceVariablesMap) } func (h *InfisicalSecretHandler) SetReadyToSyncSecretsConditions(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount int, errorToConditionOn error) { reconciler := &InfisicalSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, + Client: h.Client, + Scheme: h.Scheme, + IsNamespaceScoped: h.IsNamespaceScoped, } reconciler.SetReadyToSyncSecretsConditions(ctx, logger, infisicalSecret, secretsCount, errorToConditionOn) } func (h *InfisicalSecretHandler) SetInfisicalAutoRedeploymentReady(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) { reconciler := &InfisicalSecretReconciler{ - Client: h.Client, - Scheme: h.Scheme, + Client: h.Client, + Scheme: h.Scheme, + IsNamespaceScoped: h.IsNamespaceScoped, } reconciler.SetInfisicalAutoRedeploymentReady(ctx, logger, infisicalSecret, numDeployments, errorToConditionOn) } diff --git a/k8-operator/internal/services/infisicalsecret/reconciler.go b/k8-operator/internal/services/infisicalsecret/reconciler.go index 1c175bae3..94596fb8c 100644 --- a/k8-operator/internal/services/infisicalsecret/reconciler.go +++ b/k8-operator/internal/services/infisicalsecret/reconciler.go @@ -32,59 +32,8 @@ const FINALIZER_NAME = "secrets.finalizers.infisical.com" type InfisicalSecretReconciler struct { client.Client - Scheme *runtime.Scheme -} - -func (r *InfisicalSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) { - - // ? Legacy support, service token auth - infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret) - if err != nil { - return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) - } - if infisicalToken != "" { - infisicalClient.Auth().SetAccessToken(infisicalToken) - return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_TOKEN}, nil - } - - // ? Legacy support, service account auth - serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) - if err != nil { - return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) - } - - if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { - infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey) - return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_ACCOUNT}, nil - } - - authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){ - util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth, - util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth, - util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth, - util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth, - util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth, - util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth, - util.AuthStrategy.LDAP_MACHINE_IDENTITY: util.HandleLdapAuth, - } - - for authStrategy, authHandler := range authStrategies { - authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{ - Secret: infisicalSecret, - Type: util.SecretCrd.INFISICAL_SECRET, - }, infisicalClient) - - if err == nil { - return authDetails, nil - } - - if !errors.Is(err, util.ErrAuthNotApplicable) { - return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) - } - } - - return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided") - + Scheme *runtime.Scheme + IsNamespaceScoped bool } func (r *InfisicalSecretReconciler) getInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) { @@ -105,11 +54,14 @@ func (r *InfisicalSecretReconciler) getInfisicalTokenFromKubeSecret(ctx context. Name: secretName, }) - if k8Errors.IsNotFound(err) { + if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") { return "", nil } if err != nil { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err) + } return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err) } @@ -118,39 +70,26 @@ func (r *InfisicalSecretReconciler) getInfisicalTokenFromKubeSecret(ctx context. return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil } -func (r *InfisicalSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) { - - caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ - Namespace: infisicalSecret.Spec.TLS.CaRef.SecretNamespace, - Name: infisicalSecret.Spec.TLS.CaRef.SecretName, - }) - - if k8Errors.IsNotFound(err) { - return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err) - } - - if err != nil { - return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err) - } - - caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalSecret.Spec.TLS.CaRef.SecretKey]) - - return caCertificateFromSecret, nil -} - // Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object. // If any keys are missing or an error occurs, returns an empty object or an error object, respectively. func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) { + + secretNamespace := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace + secretName := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName + serviceAccountCredsFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ - Namespace: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace, - Name: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName, + Namespace: secretNamespace, + Name: secretName, }) - if k8Errors.IsNotFound(err) { + if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") { return model.ServiceAccountDetails{}, nil } if err != nil { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return model.ServiceAccountDetails{}, fmt.Errorf("unable to fetch Kubernetes service account credentials secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the service account credentials secret is in the same namespace as the operator. [err=%v]", err) + } return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err) } @@ -503,7 +442,11 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") - authDetails, err = r.handleAuthentication(ctx, *infisicalSecret, infisicalClient) + authDetails, err = util.HandleAuthentication(ctx, util.SecretAuthInput{ + Secret: *infisicalSecret, + Type: util.SecretCrd.INFISICAL_SECRET, + }, r.Client, infisicalClient, r.IsNamespaceScoped) + r.SetInfisicalTokenLoadCondition(ctx, logger, infisicalSecret, authDetails.AuthStrategy, err) if err != nil { @@ -533,6 +476,9 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context }) if err != nil && !k8Errors.IsNotFound(err) { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return 0, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the secret is in the same namespace as the operator. [err=%v]", err) + } return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err) } @@ -557,6 +503,9 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context }) if err != nil && !k8Errors.IsNotFound(err) { + if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) { + return 0, fmt.Errorf("unable to fetch Kubernetes config map. Your Operator installation is namespace scoped, and cannot read config maps outside of the namespace it is installed in. Please ensure the config map is in the same namespace as the operator. [err=%v]", err) + } return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes config map [%w]", err) } diff --git a/k8-operator/internal/util/auth.go b/k8-operator/internal/util/auth.go index ab5802d61..81e57def3 100644 --- a/k8-operator/internal/util/auth.go +++ b/k8-operator/internal/util/auth.go @@ -16,7 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAccountName string, autoCreateServiceAccountToken bool, serviceAccountTokenAudiences []string) (string, error) { +func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAccountName string, autoCreateServiceAccountToken bool, serviceAccountTokenAudiences []string, isNamespaceScoped bool) (string, error) { if autoCreateServiceAccountToken { restClient, err := GetRestClientFromClient() @@ -57,6 +57,9 @@ func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAc serviceAccount := &corev1.ServiceAccount{} err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, serviceAccount) if err != nil { + if IsNamespaceScopedError(err, isNamespaceScoped) { + return "", fmt.Errorf("unable to fetch service account. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the service account is in the same namespace as the operator. [err=%v]", err) + } return "", err } @@ -69,6 +72,9 @@ func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAc secret := &corev1.Secret{} err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: secretName, Namespace: namespace}, secret) if err != nil { + if IsNamespaceScopedError(err, isNamespaceScoped) { + return "", fmt.Errorf("unable to fetch service account token secret. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the service account token secret is in the same namespace as the operator. [err=%v]", err) + } return "", err } @@ -127,7 +133,7 @@ type AuthenticationDetails struct { var ErrAuthNotApplicable = errors.New("authentication not applicable") -func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) { var universalAuthSpec v1alpha1.UniversalAuthDetails @@ -164,10 +170,14 @@ func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, se } } + if universalAuthSpec.CredentialsRef.SecretName == "" || universalAuthSpec.CredentialsRef.SecretNamespace == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + universalAuthKubeSecret, err := GetInfisicalUniversalAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{ SecretNamespace: universalAuthSpec.CredentialsRef.SecretNamespace, SecretName: universalAuthSpec.CredentialsRef.SecretName, - }) + }, isNamespaceScoped) if err != nil { return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err) @@ -190,7 +200,7 @@ func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, se }, nil } -func HandleLdapAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleLdapAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) { var ldapAuthSpec v1alpha1.LdapAuthDetails @@ -229,10 +239,14 @@ func HandleLdapAuth(ctx context.Context, reconcilerClient client.Client, secretC } } + if ldapAuthSpec.CredentialsRef.SecretName == "" || ldapAuthSpec.CredentialsRef.SecretNamespace == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + ldapAuthKubeSecret, err := GetInfisicalLdapAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{ SecretNamespace: ldapAuthSpec.CredentialsRef.SecretNamespace, SecretName: ldapAuthSpec.CredentialsRef.SecretName, - }) + }, isNamespaceScoped) if err != nil { return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err) @@ -255,7 +269,7 @@ func HandleLdapAuth(ctx context.Context, reconcilerClient client.Client, secretC }, nil } -func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) { var kubernetesAuthSpec v1alpha1.KubernetesAuthDetails switch secretCrd.Type { @@ -312,6 +326,7 @@ func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, s kubernetesAuthSpec.ServiceAccountRef.Name, kubernetesAuthSpec.AutoCreateServiceAccountToken, kubernetesAuthSpec.ServiceAccountTokenAudiences, + isNamespaceScoped, ) if err != nil { @@ -332,7 +347,7 @@ func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, s } -func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) { awsIamAuthSpec := v1alpha1.AWSIamAuthDetails{} switch secretCrd.Type { @@ -387,7 +402,7 @@ func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secre } -func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) { azureAuthSpec := v1alpha1.AzureAuthDetails{} switch secretCrd.Type { @@ -445,7 +460,7 @@ func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secret } -func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) { gcpIdTokenSpec := v1alpha1.GCPIdTokenAuthDetails{} switch secretCrd.Type { @@ -500,7 +515,7 @@ func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, s } -func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) { gcpIamSpec := v1alpha1.GcpIamAuthDetails{} switch secretCrd.Type { @@ -555,3 +570,61 @@ func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secre SecretType: secretCrd.Type, }, nil } + +func HandleAuthentication(ctx context.Context, secretInput SecretAuthInput, reconcilerClient client.Client, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) { + + // We only support legacy auth for InfisicalSecret CRD + if secretInput.Type == SecretCrd.INFISICAL_SECRET { + infisicalSecret, ok := secretInput.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + + // ? Legacy support, service token auth + infisicalToken, err := GetInfisicalTokenFromKubeSecret(ctx, reconcilerClient, infisicalSecret) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) + } + if infisicalToken != "" { + infisicalClient.Auth().SetAccessToken(infisicalToken) + return AuthenticationDetails{AuthStrategy: AuthStrategy.SERVICE_TOKEN}, nil + } + + // ? Legacy support, service account auth + serviceAccountCreds, err := GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, reconcilerClient, infisicalSecret) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) + } + + if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { + infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey) + return AuthenticationDetails{AuthStrategy: AuthStrategy.SERVICE_ACCOUNT}, nil + } + } + + authStrategies := map[AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error){ + AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: HandleUniversalAuth, + AuthStrategy.KUBERNETES_MACHINE_IDENTITY: HandleKubernetesAuth, + AuthStrategy.AWS_IAM_MACHINE_IDENTITY: HandleAwsIamAuth, + AuthStrategy.AZURE_MACHINE_IDENTITY: HandleAzureAuth, + AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: HandleGcpIdTokenAuth, + AuthStrategy.GCP_IAM_MACHINE_IDENTITY: HandleGcpIamAuth, + AuthStrategy.LDAP_MACHINE_IDENTITY: HandleLdapAuth, + } + + for authStrategy, authHandler := range authStrategies { + authDetails, err := authHandler(ctx, reconcilerClient, secretInput, infisicalClient, isNamespaceScoped) + + if err == nil { + return authDetails, nil + } + + if !errors.Is(err, ErrAuthNotApplicable) { + return AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) + } + } + + return AuthenticationDetails{}, fmt.Errorf("no authentication method provided") + +} diff --git a/k8-operator/internal/util/helpers.go b/k8-operator/internal/util/helpers.go index ef3712715..c292caf3f 100644 --- a/k8-operator/internal/util/helpers.go +++ b/k8-operator/internal/util/helpers.go @@ -54,3 +54,7 @@ func AppendAPIEndpoint(address string) string { } return address + "/api" } + +func IsNamespaceScopedError(err error, isNamespaceScoped bool) bool { + return isNamespaceScoped && err != nil && strings.Contains(err.Error(), "unknown namespace for the cache") +} diff --git a/k8-operator/internal/util/kubernetes.go b/k8-operator/internal/util/kubernetes.go index 103da63eb..2f0dd0d9e 100644 --- a/k8-operator/internal/util/kubernetes.go +++ b/k8-operator/internal/util/kubernetes.go @@ -3,8 +3,10 @@ package util import ( "context" "fmt" + "strings" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/internal/constants" "github.com/Infisical/infisical/k8-operator/internal/model" corev1 "k8s.io/api/core/v1" k8Errors "k8s.io/apimachinery/pkg/api/errors" @@ -41,13 +43,11 @@ func GetKubeConfigMapByNamespacedName(ctx context.Context, reconcilerClient clie return kubeConfigMap, err } -func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference) (machineIdentityDetails model.UniversalAuthIdentityDetails, err error) { +func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference, isNamespaceScoped bool) (machineIdentityDetails model.UniversalAuthIdentityDetails, err error) { universalAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{ Namespace: universalAuthRef.SecretNamespace, Name: universalAuthRef.SecretName, - // Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace, - // Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName, }) if k8Errors.IsNotFound(err) { @@ -55,6 +55,9 @@ func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClie } if err != nil { + if IsNamespaceScopedError(err, isNamespaceScoped) { + return model.UniversalAuthIdentityDetails{}, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the secret is in the same namespace as the operator. [err=%v]", err) + } return model.UniversalAuthIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err) } @@ -65,7 +68,7 @@ func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClie } -func GetInfisicalLdapAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, ldapAuthRef v1alpha1.KubeSecretReference) (machineIdentityDetails model.LdapIdentityDetails, err error) { +func GetInfisicalLdapAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, ldapAuthRef v1alpha1.KubeSecretReference, isNamespaceScoped bool) (machineIdentityDetails model.LdapIdentityDetails, err error) { ldapAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{ Namespace: ldapAuthRef.SecretNamespace, @@ -77,6 +80,9 @@ func GetInfisicalLdapAuthFromKubeSecret(ctx context.Context, reconcilerClient cl } if err != nil { + if IsNamespaceScopedError(err, isNamespaceScoped) { + return model.LdapIdentityDetails{}, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the secret is in the same namespace as the operator. [err=%v]", err) + } return model.LdapIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err) } @@ -115,3 +121,63 @@ func GetRestClientFromClient() (rest.Interface, error) { return clientset.CoreV1().RESTClient(), nil } + +func GetInfisicalTokenFromKubeSecret(ctx context.Context, reconcilerClient client.Client, infisicalSecret v1alpha1.InfisicalSecret) (string, error) { + // default to new secret ref structure + secretName := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretName + secretNamespace := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretNamespace + // fall back to previous secret ref + if secretName == "" { + secretName = infisicalSecret.Spec.TokenSecretReference.SecretName + } + + if secretNamespace == "" { + secretNamespace = infisicalSecret.Spec.TokenSecretReference.SecretNamespace + } + + tokenSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{ + Namespace: secretNamespace, + Name: secretName, + }) + + if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") { + return "", nil + } + + if err != nil { + return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err) + } + + infisicalServiceToken := tokenSecret.Data[constants.INFISICAL_TOKEN_SECRET_KEY_NAME] + + return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil +} + +func GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, reconcilerClient client.Client, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) { + + secretNamespace := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace + secretName := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName + + serviceAccountCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{ + Namespace: secretNamespace, + Name: secretName, + }) + + if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") { + return model.ServiceAccountDetails{}, nil + } + + if err != nil { + return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err) + } + + accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_ACCESS_KEY] + publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PUBLIC_KEY] + privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PRIVATE_KEY] + + if accessKeyFromSecret == nil || publicKeyFromSecret == nil || privateKeyFromSecret == nil { + return model.ServiceAccountDetails{}, nil + } + + return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil +} diff --git a/k8-operator/scripts/generate-helm.sh b/k8-operator/scripts/generate-helm.sh index 8fd1c8d2b..87d6ec1fb 100755 --- a/k8-operator/scripts/generate-helm.sh +++ b/k8-operator/scripts/generate-helm.sh @@ -1,12 +1,36 @@ #!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) +PATH_TO_HELM_CHART="${SCRIPT_DIR}/../../helm-charts/secrets-operator" + PROJECT_ROOT=$(cd "${SCRIPT_DIR}/.." && pwd) HELM_DIR="${PROJECT_ROOT}/../helm-charts/secrets-operator" LOCALBIN="${PROJECT_ROOT}/bin" KUSTOMIZE="${LOCALBIN}/kustomize" HELMIFY="${LOCALBIN}/helmify" +VERSION=$1 +VERSION_WITHOUT_V=$(echo "$VERSION" | sed 's/^v//') # needed to validate semver + + +# Version validation +if [ -z "$VERSION" ]; then + echo "Usage: $0 " + exit 1 +fi + + +if ! [[ "$VERSION_WITHOUT_V" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must follow semantic versioning (e.g. 0.0.1)" + exit 1 +fi + +if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must start with 'v' (e.g. v0.0.1)" + exit 1 +fi + + cd "${PROJECT_ROOT}" # first run the regular helm target to generate base templates @@ -309,6 +333,48 @@ if [ -f "${HELM_DIR}/templates/deployment.yaml" ]; then echo "Completed processing for deployment.yaml" fi +# ? NOTE(Daniel): Fix args structure in deployment.yaml +if [ -f "${HELM_DIR}/templates/deployment.yaml" ]; then + echo "Fixing args structure in deployment.yaml" + + touch "${HELM_DIR}/templates/deployment.yaml.tmp" + + # process the file line by line + while IFS= read -r line; do + # look for the specific line pattern: "- args: {{- toYaml .Values.controllerManager.manager.args | nindent 8 }}" + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*args:[[:space:]]*\{\{-.*toYaml.*Values\.controllerManager\.manager\.args.*\}\}[[:space:]]*$ ]]; then + # extract the base indentation (everything before the "- args:") + base_indent=$(echo "$line" | sed 's/^\([[:space:]]*\)-.*/\1/') + + # replace with our multi-line structure + echo "${base_indent}- args:" >> "${HELM_DIR}/templates/deployment.yaml.tmp" + echo "${base_indent} {{- toYaml .Values.controllerManager.manager.args | nindent 8 }}" >> "${HELM_DIR}/templates/deployment.yaml.tmp" + echo "${base_indent} {{- if and .Values.scopedNamespace .Values.scopedRBAC }}" >> "${HELM_DIR}/templates/deployment.yaml.tmp" + echo "${base_indent} - --namespace={{ .Values.scopedNamespace }}" >> "${HELM_DIR}/templates/deployment.yaml.tmp" + echo "${base_indent} {{- end }}" >> "${HELM_DIR}/templates/deployment.yaml.tmp" + else + echo "$line" >> "${HELM_DIR}/templates/deployment.yaml.tmp" + fi + done < "${HELM_DIR}/templates/deployment.yaml" + + mv "${HELM_DIR}/templates/deployment.yaml.tmp" "${HELM_DIR}/templates/deployment.yaml" + echo "Completed args structure fix" +fi + + + + + + + + + + + + + + + # ? NOTE(Daniel): Processes values.yaml if [ -f "${HELM_DIR}/values.yaml" ]; then echo "Processing values.yaml file" @@ -402,4 +468,20 @@ if [ -f "${HELM_DIR}/values.yaml" ]; then echo "Completed processing for values.yaml" fi -echo "Helm chart generation complete with custom templating applied." \ No newline at end of file +echo "Helm chart generation complete with custom templating applied." + + + + +# For Linux vs macOS sed compatibility +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS version + sed -i '' 's/appVersion: .*/appVersion: "'"$VERSION"'"/g' "${PATH_TO_HELM_CHART}/Chart.yaml" + sed -i '' 's/version: .*/version: '"$VERSION"'/g' "${PATH_TO_HELM_CHART}/Chart.yaml" +else + # Linux version + sed -i 's/appVersion: .*/appVersion: "'"$VERSION"'"/g' "${PATH_TO_HELM_CHART}/Chart.yaml" + sed -i 's/version: .*/version: '"$VERSION"'/g' "${PATH_TO_HELM_CHART}/Chart.yaml" +fi + +echo "Helm chart version updated to ${VERSION}" \ No newline at end of file diff --git a/k8-operator/scripts/update-version.sh b/k8-operator/scripts/update-version.sh deleted file mode 100755 index e75fae10d..000000000 --- a/k8-operator/scripts/update-version.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env bash - -SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd) -PATH_TO_HELM_CHART="${SCRIPT_DIR}/../../helm-charts/secrets-operator" - -VERSION=$1 -VERSION_WITHOUT_V=$(echo "$VERSION" | sed 's/^v//') # needed to validate semver - - -if [ -z "$VERSION" ]; then - echo "Usage: $0 " - exit 1 -fi - - -if ! [[ "$VERSION_WITHOUT_V" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Version must follow semantic versioning (e.g. 0.0.1)" - exit 1 -fi - -if ! [[ "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Version must start with 'v' (e.g. v0.0.1)" - exit 1 -fi - -# For Linux vs macOS sed compatibility -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS version - sed -i '' -e '/repository: infisical\/kubernetes-operator/{n;s/tag: .*/tag: '"$VERSION"'/;}' "${PATH_TO_HELM_CHART}/values.yaml" - sed -i '' 's/appVersion: .*/appVersion: "'"$VERSION"'"/g' "${PATH_TO_HELM_CHART}/Chart.yaml" - sed -i '' 's/version: .*/version: '"$VERSION"'/g' "${PATH_TO_HELM_CHART}/Chart.yaml" -else - # Linux version - sed -i -e '/repository: infisical\/kubernetes-operator/{n;s/tag: .*/tag: '"$VERSION"'/;}' "${PATH_TO_HELM_CHART}/values.yaml" - sed -i 's/appVersion: .*/appVersion: "'"$VERSION"'"/g' "${PATH_TO_HELM_CHART}/Chart.yaml" - sed -i 's/version: .*/version: '"$VERSION"'/g' "${PATH_TO_HELM_CHART}/Chart.yaml" -fi \ No newline at end of file diff --git a/k8-operator/test/e2e/e2e_test.go b/k8-operator/test/e2e/e2e_test.go index 15b26328c..5f3830e45 100644 --- a/k8-operator/test/e2e/e2e_test.go +++ b/k8-operator/test/e2e/e2e_test.go @@ -31,16 +31,16 @@ import ( ) // namespace where the project is deployed in -const namespace = "k8-operator-system" +const namespace = "infisical-operator-system" // serviceAccountName created for the project -const serviceAccountName = "k8-operator-controller-manager" +const serviceAccountName = "infisical-operator-controller-manager" // metricsServiceName is the name of the metrics service of the project -const metricsServiceName = "k8-operator-controller-manager-metrics-service" +const metricsServiceName = "infisical-operator-controller-manager-metrics-service" // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data -const metricsRoleBindingName = "k8-operator-metrics-binding" +const metricsRoleBindingName = "infisical-operator-metrics-binding" var _ = Describe("Manager", Ordered, func() { var controllerPodName string @@ -173,7 +173,7 @@ var _ = Describe("Manager", Ordered, func() { It("should ensure the metrics endpoint is serving metrics", func() { By("creating a ClusterRoleBinding for the service account to allow access to metrics") cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, - "--clusterrole=k8-operator-metrics-reader", + "--clusterrole=infisical-operator-metrics-reader", fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), ) _, err := utils.Run(cmd)