From 498a6fe07f6c018a3cc33c910721b80598403f0e Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Sat, 4 Oct 2025 20:42:48 -0700 Subject: [PATCH 1/5] adding initial cert-manager support codebase --- PROJECT | 8 + api/v4/common_types.go | 6 + api/v4/standalone_types.go | 3 + api/v4/tls_types.go | 149 ++++ api/v4/zz_generated.deepcopy.go | 153 +++++ cmd/main.go | 204 ++++-- config/default/kustomization-cluster.yaml | 300 +++++--- config/default/kustomization-namespace.yaml | 302 +++++--- config/default/kustomization.yaml | 302 +++++--- config/default/manager_webhook_patch.yaml | 31 + config/default/metrics_service.yaml | 2 +- config/default/target-controller-manager.yaml | 5 + config/default/watch-namespace-cluster.yaml | 9 + config/default/watch-namespace-namespace.yaml | 13 + config/manager/kustomization.yaml | 4 +- config/manager/manager.yaml | 46 +- .../network-policy/allow-metrics-traffic.yaml | 27 + .../network-policy/allow-webhook-traffic.yaml | 27 + config/network-policy/kustomization.yaml | 3 + config/webhook/kustomization.yaml | 6 + config/webhook/kustomizeconfig.yaml | 22 + config/webhook/manifests.yaml | 52 ++ config/webhook/service.yaml | 16 + go.mod | 27 +- go.sum | 78 ++- internal/webhook/v4/standalone_webhook.go | 134 ++++ .../webhook/v4/standalone_webhook_test.go | 87 +++ internal/webhook/v4/webhook_suite_test.go | 164 +++++ pkg/splunk/enterprise/standalone.go | 10 +- pkg/splunk/enterprise/standalone_status.go | 37 + pkg/splunk/enterprise/tls_configuraiton.go | 118 ++++ pkg/terms/terms.go | 23 + pkg/tls/constants.go | 41 ++ pkg/tls/defaults.go | 82 +++ pkg/tls/mutate.go | 232 +++++++ pkg/tls/pretasks_renderer.go | 63 ++ pkg/tls/pretasks_renderer_template_test.go | 204 ++++++ pkg/tls/pretasks_renderer_test.go | 143 ++++ pkg/tls/status.go | 311 +++++++++ pkg/tls/templates/pretasks.tmpl.yaml | 647 ++++++++++++++++++ pkg/tls/validate.go | 21 + 41 files changed, 3718 insertions(+), 394 deletions(-) create mode 100644 api/v4/tls_types.go create mode 100644 config/default/manager_webhook_patch.yaml create mode 100644 config/default/target-controller-manager.yaml create mode 100644 config/default/watch-namespace-cluster.yaml create mode 100644 config/default/watch-namespace-namespace.yaml create mode 100644 config/network-policy/allow-metrics-traffic.yaml create mode 100644 config/network-policy/allow-webhook-traffic.yaml create mode 100644 config/network-policy/kustomization.yaml create mode 100644 config/webhook/kustomization.yaml create mode 100644 config/webhook/kustomizeconfig.yaml create mode 100644 config/webhook/manifests.yaml create mode 100644 config/webhook/service.yaml create mode 100644 internal/webhook/v4/standalone_webhook.go create mode 100644 internal/webhook/v4/standalone_webhook_test.go create mode 100644 internal/webhook/v4/webhook_suite_test.go create mode 100644 pkg/splunk/enterprise/standalone_status.go create mode 100644 pkg/splunk/enterprise/tls_configuraiton.go create mode 100644 pkg/terms/terms.go create mode 100644 pkg/tls/constants.go create mode 100644 pkg/tls/defaults.go create mode 100644 pkg/tls/mutate.go create mode 100644 pkg/tls/pretasks_renderer.go create mode 100644 pkg/tls/pretasks_renderer_template_test.go create mode 100644 pkg/tls/pretasks_renderer_test.go create mode 100644 pkg/tls/status.go create mode 100644 pkg/tls/templates/pretasks.tmpl.yaml create mode 100644 pkg/tls/validate.go diff --git a/PROJECT b/PROJECT index 62abf2007..5919e1dbb 100644 --- a/PROJECT +++ b/PROJECT @@ -1,3 +1,7 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: splunk.com layout: - go.kubebuilder.io/v4 @@ -77,6 +81,10 @@ resources: kind: Standalone path: github.com/splunk/splunk-operator/api/v4 version: v4 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: true diff --git a/api/v4/common_types.go b/api/v4/common_types.go index 5bba9c0cd..0c01ba69d 100644 --- a/api/v4/common_types.go +++ b/api/v4/common_types.go @@ -93,6 +93,7 @@ type Spec struct { // Sets pull policy for all images (either “Always” or the default: “IfNotPresent”) // +kubebuilder:validation:Enum=Always;IfNotPresent + // +kubebuilder:default:=IfNotPresent ImagePullPolicy string `json:"imagePullPolicy"` // Name of Scheduler to use for pod placement (defaults to “default-scheduler”) @@ -170,6 +171,8 @@ type CommonSplunkSpec struct { VarVolumeStorageConfig StorageClassSpec `json:"varVolumeStorageConfig"` // List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ + // +kubebuilder:validation:Optional + // +kubebuilder:default:={} Volumes []corev1.Volume `json:"volumes"` // Inline map of default.yml overrides used to initialize the environment @@ -238,6 +241,9 @@ type CommonSplunkSpec struct { // Sets imagePullSecrets if image is being pulled from a private registry. // See https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + + // +optional + TLS *TLSConfig `json:"tls,omitempty"` } // StorageClassSpec defines storage class configuration diff --git a/api/v4/standalone_types.go b/api/v4/standalone_types.go index 45220958c..1d69d077c 100644 --- a/api/v4/standalone_types.go +++ b/api/v4/standalone_types.go @@ -76,6 +76,9 @@ type StandaloneStatus struct { // Auxillary message describing CR status Message string `json:"message"` + + // +optional + TLS *TLSStatus `json:"tls,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/api/v4/tls_types.go b/api/v4/tls_types.go new file mode 100644 index 000000000..0a1c691f5 --- /dev/null +++ b/api/v4/tls_types.go @@ -0,0 +1,149 @@ +/* +Copyright (c) 2018-2022 Splunk Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TLS and TrustBundle types shared by all CRDs. + +type TLSProvider string +type TLSConditionType string + +const ( + TLSProviderSecret TLSProvider = "Secret" + TLSProviderCSI TLSProvider = "CSI" +) + +const ( + TLSReady TLSConditionType = "TLSReady" + TLSRotatePending TLSConditionType = "TLSRotatePending" + TLSTrustBundleReady TLSConditionType = "TLSTrustBundleReady" + TLSTrackingLimited TLSConditionType = "TLSTrackingLimited" // CSI without a trackable Secret +) + +type TLSSecretRef struct { + // Secret with keys: tls.crt, tls.key, ca.crt (optional) + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` +} + +type TLSCSI struct { + // cert-manager CSI driver attributes + // +kubebuilder:validation:Enum=Issuer;ClusterIssuer + IssuerRefKind string `json:"issuerRefKind"` + // +kubebuilder:validation:MinLength=1 + IssuerRefName string `json:"issuerRefName"` + // DNS SANs for the leaf + // +optional + DNSNames []string `json:"dnsNames,omitempty"` + // Durations parsed by cert-manager (example "2160h") + // +optional + Duration string `json:"duration,omitempty"` + // +optional + RenewBefore string `json:"renewBefore,omitempty"` + // +kubebuilder:validation:Enum=rsa;ecdsa + // +optional + KeyAlgorithm string `json:"keyAlgorithm,omitempty"` + // +optional + KeySize int `json:"keySize,omitempty"` +} + +type TrustBundle struct { + // Optional trust-manager bundle Secret containing the CA bundle + // +optional + SecretName string `json:"secretName,omitempty"` + // +optional + Key string `json:"key,omitempty"` // default: "ca-bundle.crt" +} + +// TLSConfig controls how certs/keys/CA are presented to Splunk. +// All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. +type TLSConfig struct { + // +kubebuilder:validation:Enum=Secret;CSI + Provider TLSProvider `json:"provider"` + // +optional + SecretRef *TLSSecretRef `json:"secretRef,omitempty"` + // +optional + CSI *TLSCSI `json:"csi,omitempty"` + // Canonical destination inside $SPLUNK_HOME, defaults to /opt/splunk/etc/auth/tls + // +optional + CanonicalDir string `json:"canonicalDir,omitempty"` + // Optional Trust Bundle mounted as a Secret produced by trust-manager + // +optional + TrustBundle *TrustBundle `json:"trustBundle,omitempty"` + + // KVEncryptedKey enables building a separate PEM for KV store + // that contains an AES-256 encrypted private key, and writes + // [kvstore] sslPassword + serverCert in server.conf accordingly. + // Defaults to disabled for simplicity and reliability. + KVEncryptedKey *KVEncryptedKeySpec `json:"kvEncryptedKey,omitempty"` +} + +type KVEncryptedKeySpec struct { + // Enabled toggles the feature on or off. Default: false. + Enabled bool `json:"enabled"` + + // PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + // to encrypt the key. If omitted, we will auto-generate a random + // base64 passphrase at runtime inside the pod. + PasswordSecretRef *corev1.SecretKeySelector `json:"passwordSecretRef,omitempty"` + + // BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + // Default: "kvstore.pem" + BundleFile string `json:"bundleFile,omitempty"` +} + +type TLSCondition struct { + Type TLSConditionType `json:"type"` + Status corev1.ConditionStatus `json:"status"` + Reason string `json:"reason,omitempty"` + Message string `json:"message,omitempty"` + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` +} + +type TLSObserved struct { + // Where we sourced material from - e.g. "Secret/" or "CSI:Issuer/Name" + Source string `json:"source,omitempty"` + // Hash over cert material we observe + // - for Secret: sha256 over tls.crt, tls.key, ca.crt if present + // - for CSI: sha256 over CSI attributes (best-effort) + Hash string `json:"hash,omitempty"` + + // Leaf certificate facts if we can parse tls.crt + LeafSHA256 string `json:"leafSHA256,omitempty"` + NotBefore *metav1.Time `json:"notBefore,omitempty"` + NotAfter *metav1.Time `json:"notAfter,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` + + // Secret resourceVersion if provider=Secret + SecretResourceVersion string `json:"secretResourceVersion,omitempty"` +} + +// +kubebuilder:object:generate=true +type TLSStatus struct { + Provider TLSProvider `json:"provider,omitempty"` + CanonicalDir string `json:"canonicalDir,omitempty"` + Observed TLSObserved `json:"observed,omitempty"` + TrustBundleHash string `json:"trustBundleHash,omitempty"` + PreTasksHash string `json:"preTasksHash,omitempty"` // NEW + PodTemplateChecksum string `json:"podTemplateChecksum,omitempty"` // NEW + Conditions []TLSCondition `json:"conditions,omitempty"` + LastObserved metav1.Time `json:"lastObserved,omitempty"` +} diff --git a/api/v4/zz_generated.deepcopy.go b/api/v4/zz_generated.deepcopy.go index 93e988463..34f47efa4 100644 --- a/api/v4/zz_generated.deepcopy.go +++ b/api/v4/zz_generated.deepcopy.go @@ -343,6 +343,11 @@ func (in *CommonSplunkSpec) DeepCopyInto(out *CommonSplunkSpec) { *out = make([]v1.LocalObjectReference, len(*in)) copy(*out, *in) } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonSplunkSpec. @@ -1076,6 +1081,11 @@ func (in *StandaloneStatus) DeepCopyInto(out *StandaloneStatus) { } } in.AppContext.DeepCopyInto(&out.AppContext) + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(TLSStatus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StandaloneStatus. @@ -1103,6 +1113,149 @@ func (in *StorageClassSpec) DeepCopy() *StorageClassSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSCSI) DeepCopyInto(out *TLSCSI) { + *out = *in + if in.DNSNames != nil { + in, out := &in.DNSNames, &out.DNSNames + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCSI. +func (in *TLSCSI) DeepCopy() *TLSCSI { + if in == nil { + return nil + } + out := new(TLSCSI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSCondition) DeepCopyInto(out *TLSCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCondition. +func (in *TLSCondition) DeepCopy() *TLSCondition { + if in == nil { + return nil + } + out := new(TLSCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(TLSSecretRef) + **out = **in + } + if in.CSI != nil { + in, out := &in.CSI, &out.CSI + *out = new(TLSCSI) + (*in).DeepCopyInto(*out) + } + if in.TrustBundle != nil { + in, out := &in.TrustBundle, &out.TrustBundle + *out = new(TrustBundle) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig. +func (in *TLSConfig) DeepCopy() *TLSConfig { + if in == nil { + return nil + } + out := new(TLSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSObserved) DeepCopyInto(out *TLSObserved) { + *out = *in + if in.NotBefore != nil { + in, out := &in.NotBefore, &out.NotBefore + *out = (*in).DeepCopy() + } + if in.NotAfter != nil { + in, out := &in.NotAfter, &out.NotAfter + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSObserved. +func (in *TLSObserved) DeepCopy() *TLSObserved { + if in == nil { + return nil + } + out := new(TLSObserved) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSecretRef) DeepCopyInto(out *TLSSecretRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSecretRef. +func (in *TLSSecretRef) DeepCopy() *TLSSecretRef { + if in == nil { + return nil + } + out := new(TLSSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSStatus) DeepCopyInto(out *TLSStatus) { + *out = *in + in.Observed.DeepCopyInto(&out.Observed) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]TLSCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.LastObserved.DeepCopyInto(&out.LastObserved) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSStatus. +func (in *TLSStatus) DeepCopy() *TLSStatus { + if in == nil { + return nil + } + out := new(TLSStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustBundle) DeepCopyInto(out *TrustBundle) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustBundle. +func (in *TrustBundle) DeepCopy() *TrustBundle { + if in == nil { + return nil + } + out := new(TrustBundle) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeAndTypeSpec) DeepCopyInto(out *VolumeAndTypeSpec) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 6a152ce16..e2b6fda56 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,12 +21,19 @@ import ( "flag" "fmt" "os" - "time" + "path/filepath" + + //"time" + + "sigs.k8s.io/controller-runtime/pkg/certwatcher" + "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + "sigs.k8s.io/controller-runtime/pkg/webhook" intController "github.com/splunk/splunk-operator/internal/controller" "github.com/splunk/splunk-operator/internal/controller/debug" - "github.com/splunk/splunk-operator/pkg/config" - "sigs.k8s.io/controller-runtime/pkg/metrics/filters" + "github.com/splunk/splunk-operator/pkg/terms" + + //"github.com/splunk/splunk-operator/pkg/config" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -34,7 +41,7 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" - "go.uber.org/zap/zapcore" + //"go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -47,6 +54,7 @@ import ( enterpriseApiV3 "github.com/splunk/splunk-operator/api/v3" enterpriseApi "github.com/splunk/splunk-operator/api/v4" + webhookv4 "github.com/splunk/splunk-operator/internal/webhook/v4" //+kubebuilder:scaffold:imports //extapi "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) @@ -65,99 +73,150 @@ func init() { } func main() { + terms.InitFromEnv() var metricsAddr string - var secureMetrics bool + var metricsCertPath, metricsCertName, metricsCertKey string + var webhookCertPath, webhookCertName, webhookCertKey string var enableLeaderElection bool var probeAddr string + var secureMetrics bool + var enableHTTP2 bool var pprofActive bool - var logEncoder string - var logLevel int - - var leaseDuration time.Duration - var renewDeadline time.Duration - var leaseDurationSecond int - var renewDeadlineSecond int - var tlsOpts []func(*tls.Config) - - flag.StringVar(&logEncoder, "log-encoder", "json", "log encoding ('json' or 'console')") + 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.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") - flag.BoolVar(&pprofActive, "pprof", true, "Enable pprof endpoint") - flag.IntVar(&logLevel, "log-level", int(zapcore.InfoLevel), "set log level") - flag.IntVar(&leaseDurationSecond, "lease-duration", int(leaseDurationSecond), "manager lease duration in seconds") - flag.IntVar(&renewDeadlineSecond, "renew-duration", int(renewDeadlineSecond), "manager renew duration in seconds") - flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "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.BoolVar(&secureMetrics, "metrics-secure", false, + flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") + flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") + flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") + flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") + flag.StringVar(&metricsCertPath, "metrics-cert-path", "", + "The directory that contains the metrics server certificate.") + flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") + flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") + flag.BoolVar(&enableHTTP2, "enable-http2", false, + "If set, HTTP/2 will be enabled for the metrics and webhook servers") + flag.BoolVar(&pprofActive, "pprof", true, "Enable pprof endpoint") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + // if the enable-http2 flag is false (the default), http/2 should be disabled + // due to its vulnerabilities. More specifically, disabling http/2 will + // prevent from being vulnerable to the HTTP/2 Stream Cancellation and + // Rapid Reset CVEs. For more information see: + // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 + // - https://github.com/advisories/GHSA-4374-p667-p6c8 + disableHTTP2 := func(c *tls.Config) { + setupLog.Info("disabling http/2") + c.NextProtos = []string{"http/1.1"} + } + + if !enableHTTP2 { + tlsOpts = append(tlsOpts, disableHTTP2) + } + + // Create watchers for metrics and webhooks certificates + var metricsCertWatcher, webhookCertWatcher *certwatcher.CertWatcher + + // Initial webhook TLS options + webhookTLSOpts := tlsOpts + + if len(webhookCertPath) > 0 { + setupLog.Info("Initializing webhook certificate watcher using provided certificates", + "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) + + var err error + webhookCertWatcher, err = certwatcher.New( + filepath.Join(webhookCertPath, webhookCertName), + filepath.Join(webhookCertPath, webhookCertKey), + ) + if err != nil { + setupLog.Error(err, "Failed to initialize webhook certificate watcher") + os.Exit(1) + } + + webhookTLSOpts = append(webhookTLSOpts, func(config *tls.Config) { + config.GetCertificate = webhookCertWatcher.GetCertificate + }) + } + + webhookServer := webhook.NewServer(webhook.Options{ + TLSOpts: webhookTLSOpts, + }) // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: - // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/server + // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, - // TODO(user): TLSOpts is used to allow configuring the TLS config used for the server. If certificates are - // not provided, self-signed certificates will be generated by default. This option is not recommended for - // production environments as self-signed certificates do not offer the same level of trust and security - // as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing - // unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName - // to provide certificates, ensuring the server communicates using trusted and secure certificates. - TLSOpts: tlsOpts, - FilterProvider: filters.WithAuthenticationAndAuthorization, + TLSOpts: tlsOpts, } - // TODO: enable https for /metrics endpoint by default - // if secureMetrics { - // // FilterProvider is used to protect the metrics endpoint with authn/authz. - // // These configurations ensure that only authorized users and service accounts - // // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: - // // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/metrics/filters#WithAuthenticationAndAuthorization - // metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization - // } - - // see https://github.com/operator-framework/operator-sdk/issues/1813 - if leaseDurationSecond < 30 { - leaseDuration = 30 * time.Second - } else { - leaseDuration = time.Duration(leaseDurationSecond) * time.Second + if secureMetrics { + // FilterProvider is used to protect the metrics endpoint with authn/authz. + // These configurations ensure that only authorized users and service accounts + // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: + // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/metrics/filters#WithAuthenticationAndAuthorization + metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } - if renewDeadlineSecond < 20 { - renewDeadline = 20 * time.Second - } else { - renewDeadline = time.Duration(renewDeadlineSecond) * time.Second - } + // If the certificate is not specified, controller-runtime will automatically + // generate self-signed certificates for the metrics server. While convenient for development and testing, + // this setup is not recommended for production. + // + // TODO(user): If you enable certManager, uncomment the following lines: + // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates + // managed by cert-manager for the metrics server. + // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. + if len(metricsCertPath) > 0 { + setupLog.Info("Initializing metrics certificate watcher using provided certificates", + "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) - opts := zap.Options{ - Development: true, - TimeEncoder: zapcore.RFC3339NanoTimeEncoder, - } - opts.BindFlags(flag.CommandLine) - flag.Parse() + var err error + metricsCertWatcher, err = certwatcher.New( + filepath.Join(metricsCertPath, metricsCertName), + filepath.Join(metricsCertPath, metricsCertKey), + ) + if err != nil { + setupLog.Error(err, "to initialize metrics certificate watcher", "error", err) + os.Exit(1) + } - // Logging setup - ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + metricsServerOptions.TLSOpts = append(metricsServerOptions.TLSOpts, func(config *tls.Config) { + config.GetCertificate = metricsCertWatcher.GetCertificate + }) + } - baseOptions := ctrl.Options{ - Metrics: metricsServerOptions, + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, + Metrics: metricsServerOptions, + WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "270bec8c.splunk.com", - LeaseDuration: &leaseDuration, - RenewDeadline: &renewDeadline, - } - - // Apply namespace-specific configuration - managerOptions := config.ManagerOptionsWithNamespaces(setupLog, baseOptions) - - mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), managerOptions) - + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // 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, + }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) @@ -221,6 +280,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Standalone") os.Exit(1) } + // nolint:goconst + if os.Getenv("ENABLE_WEBHOOKS") != "false" { + if err := webhookv4.SetupStandaloneWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Standalone") + os.Exit(1) + } + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/default/kustomization-cluster.yaml b/config/default/kustomization-cluster.yaml index 15c98e24a..d42834fdc 100644 --- a/config/default/kustomization-cluster.yaml +++ b/config/default/kustomization-cluster.yaml @@ -1,5 +1,5 @@ # Adds namespace to all resources. -namespace: splunk-operator +namespace: splunk-operator # Value of this field is prepended to the # names of all resources, e.g. a deployment named @@ -9,10 +9,12 @@ namespace: splunk-operator namePrefix: splunk-operator- # Labels to add to all resources and selectors. -commonLabels: - name: splunk-operator +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue -bases: +resources: - ../crd - ../rbac - ../persistent-volume @@ -20,96 +22,20 @@ bases: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../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 -patchesStrategicMerge: -# Mount the controller config file for loading manager configurations -# through a ComponentConfig type -#- manager_config_patch.yaml - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- manager_webhook_patch.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. -# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. -# 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml - -# the following config is for teaching kustomize how to do var substitution -vars: -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldref: -# fieldpath: metadata.namespace -#- name: CERTIFICATE_NAME -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -#- name: SERVICE_NAMESPACE # namespace of the service -# objref: -# kind: Service -# version: v1 -# name: webhook-service -# fieldref: -# fieldpath: metadata.namespace -#- name: SERVICE_NAME -# objref: -# kind: Service -# version: v1 -# name: webhook-service - -#patches: -#- target: -# kind: Deployment -# name: controller-manager -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator -#- target: -# kind: ServiceAccount -# name: controller-manager -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator -#- target: -# kind: Service -# name: controller-manager-service -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator-service -#- target: -# kind: Role -# name: manager-role -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk:operator:namespace-manager -#- target: -# kind: RoleBinding -# name: manager-rolebinding -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk:operator:namespace-manager - -# currently patch is set to change deployment environment variables +# Uncomment the patches line if you enable Metrics patches: - target: kind: Deployment @@ -119,13 +45,13 @@ patches: path: /spec/template/spec/containers/0/env value: - name: WATCH_NAMESPACE - value: WATCH_NAMESPACE_VALUE + value: "" - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: SPLUNK_ENTERPRISE_IMAGE - name: OPERATOR_NAME value: splunk-operator - name: SPLUNK_GENERAL_TERMS - value: SPLUNK_GENERAL_TERMS_VALUE + value: WATCH_NAMESPACE_VALUE - name: POD_NAME valueFrom: fieldRef: @@ -134,4 +60,196 @@ patches: # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: - kind: Deployment \ No newline at end of file + 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 diff --git a/config/default/kustomization-namespace.yaml b/config/default/kustomization-namespace.yaml index 1053b4785..6cc88f5f6 100644 --- a/config/default/kustomization-namespace.yaml +++ b/config/default/kustomization-namespace.yaml @@ -1,5 +1,5 @@ # Adds namespace to all resources. -namespace: splunk-operator +namespace: splunk-operator # Value of this field is prepended to the # names of all resources, e.g. a deployment named @@ -9,10 +9,12 @@ namespace: splunk-operator namePrefix: splunk-operator- # Labels to add to all resources and selectors. -commonLabels: - name: splunk-operator +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue -bases: +resources: - ../crd - ../rbac - ../persistent-volume @@ -20,96 +22,20 @@ bases: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../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 -patchesStrategicMerge: -# Mount the controller config file for loading manager configurations -# through a ComponentConfig type -#- manager_config_patch.yaml - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- manager_webhook_patch.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. -# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. -# 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml - -# the following config is for teaching kustomize how to do var substitution -vars: -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldref: -# fieldpath: metadata.namespace -#- name: CERTIFICATE_NAME -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -#- name: SERVICE_NAMESPACE # namespace of the service -# objref: -# kind: Service -# version: v1 -# name: webhook-service -# fieldref: -# fieldpath: metadata.namespace -#- name: SERVICE_NAME -# objref: -# kind: Service -# version: v1 -# name: webhook-service - -#patches: -#- target: -# kind: Deployment -# name: controller-manager -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator -#- target: -# kind: ServiceAccount -# name: controller-manager -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator -#- target: -# kind: Service -# name: controller-manager-service -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator-service -#- target: -# kind: Role -# name: manager-role -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk:operator:namespace-manager -#- target: -# kind: RoleBinding -# name: manager-rolebinding -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk:operator:namespace-manager - -# currently patch is set to change deployment environment variables +# Uncomment the patches line if you enable Metrics patches: - target: kind: Deployment @@ -119,15 +45,15 @@ patches: path: /spec/template/spec/containers/0/env value: - name: WATCH_NAMESPACE - valueFrom: - fieldRef: + valueFrom: + fieldRef: fieldPath: metadata.namespace - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: SPLUNK_ENTERPRISE_IMAGE - name: OPERATOR_NAME value: splunk-operator - name: SPLUNK_GENERAL_TERMS - value: SPLUNK_GENERAL_TERMS_VALUE + value: WATCH_NAMESPACE_VALUE - name: POD_NAME valueFrom: fieldRef: @@ -136,4 +62,196 @@ patches: # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: - kind: Deployment \ No newline at end of file + 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 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 15c98e24a..6cc88f5f6 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,5 +1,5 @@ # Adds namespace to all resources. -namespace: splunk-operator +namespace: splunk-operator # Value of this field is prepended to the # names of all resources, e.g. a deployment named @@ -9,10 +9,12 @@ namespace: splunk-operator namePrefix: splunk-operator- # Labels to add to all resources and selectors. -commonLabels: - name: splunk-operator +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue -bases: +resources: - ../crd - ../rbac - ../persistent-volume @@ -20,96 +22,20 @@ bases: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -#- ../certmanager +- ../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 -patchesStrategicMerge: -# Mount the controller config file for loading manager configurations -# through a ComponentConfig type -#- manager_config_patch.yaml - -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in -# crd/kustomization.yaml -#- manager_webhook_patch.yaml - -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. -# Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. -# 'CERTMANAGER' needs to be enabled to use ca injection -#- webhookcainjection_patch.yaml - -# the following config is for teaching kustomize how to do var substitution -vars: -# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -# fieldref: -# fieldpath: metadata.namespace -#- name: CERTIFICATE_NAME -# objref: -# kind: Certificate -# group: cert-manager.io -# version: v1 -# name: serving-cert # this name should match the one in certificate.yaml -#- name: SERVICE_NAMESPACE # namespace of the service -# objref: -# kind: Service -# version: v1 -# name: webhook-service -# fieldref: -# fieldpath: metadata.namespace -#- name: SERVICE_NAME -# objref: -# kind: Service -# version: v1 -# name: webhook-service - -#patches: -#- target: -# kind: Deployment -# name: controller-manager -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator -#- target: -# kind: ServiceAccount -# name: controller-manager -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator -#- target: -# kind: Service -# name: controller-manager-service -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk-operator-service -#- target: -# kind: Role -# name: manager-role -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk:operator:namespace-manager -#- target: -# kind: RoleBinding -# name: manager-rolebinding -# patch: |- -# - op: replace -# path: /metadata/name -# value: splunk:operator:namespace-manager - -# currently patch is set to change deployment environment variables +# Uncomment the patches line if you enable Metrics patches: - target: kind: Deployment @@ -119,13 +45,15 @@ patches: path: /spec/template/spec/containers/0/env value: - name: WATCH_NAMESPACE - value: WATCH_NAMESPACE_VALUE + valueFrom: + fieldRef: + fieldPath: metadata.namespace - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: SPLUNK_ENTERPRISE_IMAGE - name: OPERATOR_NAME value: splunk-operator - name: SPLUNK_GENERAL_TERMS - value: SPLUNK_GENERAL_TERMS_VALUE + value: WATCH_NAMESPACE_VALUE - name: POD_NAME valueFrom: fieldRef: @@ -134,4 +62,196 @@ patches: # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: - kind: Deployment \ No newline at end of file + 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 diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 000000000..963c8a4cc --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,31 @@ +# This patch ensures the webhook certificates are properly mounted in the manager container. +# It configures the necessary arguments, volumes, volume mounts, and container ports. + +# Add the --webhook-cert-path argument for configuring the webhook certificate path +- op: add + path: /spec/template/spec/containers/0/args/- + value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs + +# Add the volumeMount for the webhook certificates +- op: add + path: /spec/template/spec/containers/0/volumeMounts/- + value: + mountPath: /tmp/k8s-webhook-server/serving-certs + name: webhook-certs + readOnly: true + +# Add the port configuration for the webhook server +- op: add + path: /spec/template/spec/containers/0/ports/- + value: + containerPort: 9443 + name: webhook-server + protocol: TCP + +# Add the volume configuration for the webhook certificates +- op: add + path: /spec/template/spec/volumes/- + value: + name: webhook-certs + secret: + secretName: webhook-server-cert diff --git a/config/default/metrics_service.yaml b/config/default/metrics_service.yaml index cebb2683b..bb6a38a20 100644 --- a/config/default/metrics_service.yaml +++ b/config/default/metrics_service.yaml @@ -3,7 +3,7 @@ kind: Service metadata: labels: control-plane: controller-manager - app.kubernetes.io/name: controller-manager + app.kubernetes.io/name: splunk-operator app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system diff --git a/config/default/target-controller-manager.yaml b/config/default/target-controller-manager.yaml new file mode 100644 index 000000000..55ceb4338 --- /dev/null +++ b/config/default/target-controller-manager.yaml @@ -0,0 +1,5 @@ +target: + group: apps + version: v1 + kind: Deployment + name: controller-manager \ No newline at end of file diff --git a/config/default/watch-namespace-cluster.yaml b/config/default/watch-namespace-cluster.yaml new file mode 100644 index 000000000..d75d17ebe --- /dev/null +++ b/config/default/watch-namespace-cluster.yaml @@ -0,0 +1,9 @@ +- op: add + path: /spec/template/spec/containers/0/env/- + value: "" + +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SPLUNK_GENERAL_TERMS + value: "--accept-sgt-current-at-splunk-com" \ No newline at end of file diff --git a/config/default/watch-namespace-namespace.yaml b/config/default/watch-namespace-namespace.yaml new file mode 100644 index 000000000..03fa421f3 --- /dev/null +++ b/config/default/watch-namespace-namespace.yaml @@ -0,0 +1,13 @@ +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: SPLUNK_GENERAL_TERMS + value: "--accept-sgt-current-at-splunk-com" \ No newline at end of file diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 47f07b0e6..49dfd7367 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -16,5 +16,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: docker.io/splunk/splunk-operator - newTag: 3.0.0 + newName: vivekrsplunk/splunk-operator + newTag: crt-12 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 3974d02f0..d8f7c5f1d 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -3,6 +3,8 @@ kind: Namespace metadata: labels: control-plane: controller-manager + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize name: system --- apiVersion: apps/v1 @@ -12,28 +14,51 @@ metadata: namespace: system labels: control-plane: controller-manager + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: control-plane: controller-manager - name: splunk-operator + app.kubernetes.io/name: splunk-operator strategy: type: Recreate replicas: 1 template: metadata: - labels: - control-plane: controller-manager - name: splunk-operator annotations: - kubectl.kubernetes.io/default-logs-container: manager kubectl.kubernetes.io/default-container: manager + labels: + control-plane: controller-manager + app.kubernetes.io/name: splunk-operator spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux securityContext: runAsUser: 1001 fsGroup: 1001 runAsNonRoot: true fsGroupChangePolicy: "OnRootMismatch" + seccompProfile: + type: RuntimeDefault hostNetwork: false hostPID: false hostIPC: false @@ -41,12 +66,13 @@ spec: - command: - /manager args: - - --leader-elect - - --health-probe-bind-address=:8081 - - --pprof + - --leader-elect + - --health-probe-bind-address=:8081 + - --pprof image: controller:latest imagePullPolicy: Always name: manager + ports: [] env: - name: POD_NAME valueFrom: @@ -58,7 +84,7 @@ spec: runAsNonRoot: true capabilities: drop: - - "ALL" + - "ALL" add: - "NET_BIND_SERVICE" seccompProfile: @@ -75,6 +101,8 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: cpu: 1000m diff --git a/config/network-policy/allow-metrics-traffic.yaml b/config/network-policy/allow-metrics-traffic.yaml new file mode 100644 index 000000000..d121aa9ae --- /dev/null +++ b/config/network-policy/allow-metrics-traffic.yaml @@ -0,0 +1,27 @@ +# This NetworkPolicy allows ingress traffic +# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those +# namespaces are able to gather data from the metrics endpoint. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: allow-metrics-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: splunk-operator + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label metrics: enabled + - from: + - namespaceSelector: + matchLabels: + metrics: enabled # Only from namespaces with this label + ports: + - port: 8443 + protocol: TCP diff --git a/config/network-policy/allow-webhook-traffic.yaml b/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 000000000..be8cd2b7b --- /dev/null +++ b/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,27 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + app.kubernetes.io/name: splunk-operator + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/config/network-policy/kustomization.yaml b/config/network-policy/kustomization.yaml new file mode 100644 index 000000000..0872bee12 --- /dev/null +++ b/config/network-policy/kustomization.yaml @@ -0,0 +1,3 @@ +resources: +- allow-webhook-traffic.yaml +- allow-metrics-traffic.yaml diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 000000000..9cf26134e --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 000000000..206316e54 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 000000000..4c9ee7d93 --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,52 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-enterprise-splunk-com-v4-standalone + failurePolicy: Fail + name: mstandalone-v4.kb.io + rules: + - apiGroups: + - enterprise.splunk.com + apiVersions: + - v4 + operations: + - CREATE + - UPDATE + resources: + - standalones + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-enterprise-splunk-com-v4-standalone + failurePolicy: Fail + name: vstandalone-v4.kb.io + rules: + - apiGroups: + - enterprise.splunk.com + apiVersions: + - v4 + operations: + - CREATE + - UPDATE + resources: + - standalones + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 000000000..3dce1699d --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: splunk-operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager + app.kubernetes.io/name: splunk-operator diff --git a/go.mod b/go.mod index 8f24791da..9a87e5cf7 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,13 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.71 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.85 github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1 - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/minio/minio-go/v7 v7.0.16 - github.com/onsi/ginkgo/v2 v2.23.4 - github.com/onsi/gomega v1.38.0 + github.com/onsi/ginkgo/v2 v2.26.0 + github.com/onsi/gomega v1.38.2 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.19.1 github.com/stretchr/testify v1.9.0 @@ -31,6 +31,7 @@ require ( k8s.io/client-go v0.31.0 k8s.io/kubectl v0.26.2 sigs.k8s.io/controller-runtime v0.19.0 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -39,6 +40,7 @@ require ( cloud.google.com/go/iam v1.1.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect @@ -126,16 +128,18 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.39.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect - golang.org/x/net v0.41.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.27.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.6.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/tools v0.36.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect @@ -143,7 +147,7 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/grpc v1.65.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.7 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.4 // indirect @@ -157,5 +161,4 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 107db504c..ab8e20b8a 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.1/go.mod h1:ap1dmS6vQK github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= @@ -115,9 +117,15 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.14 h1:3fAqdB6BCPKHDMHAKRwtPUwYexKtGrNuw8HX/T/4neo= +github.com/gkampitakis/go-snaps v0.5.14/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= @@ -131,6 +139,8 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -201,6 +211,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -224,6 +236,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= github.com/minio/minio-go/v7 v7.0.16 h1:GspaSBS8lOuEUCAqMe0W3UxSoyOA4b4F8PTspRVI+k4= @@ -245,10 +261,10 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= -github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= +github.com/onsi/ginkgo/v2 v2.26.0 h1:1J4Wut1IlYZNEAWIV3ALrT9NfiaGW2cDCJQSFQMs/gE= +github.com/onsi/ginkgo/v2 v2.26.0/go.mod h1:qhEywmzWTBUY88kfO0BRvX4py7scov9yR+Az2oavUzw= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -268,8 +284,8 @@ github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65 github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -299,6 +315,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/wk8/go-ordered-map/v2 v2.1.7 h1:aUZ1xBMdbvY8wnNt77qqo4nyT3y0pX4Usat48Vm+hik= github.com/wk8/go-ordered-map/v2 v2.1.7/go.mod h1:9Xvgm2mV2kSq2SAm0Y608tBmu8akTzI7c2bz7/G7ZN4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -333,14 +357,16 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= @@ -350,6 +376,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -366,12 +394,10 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -381,8 +407,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -397,21 +423,21 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -424,8 +450,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -471,8 +497,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= +google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/webhook/v4/standalone_webhook.go b/internal/webhook/v4/standalone_webhook.go new file mode 100644 index 000000000..ba410e2c1 --- /dev/null +++ b/internal/webhook/v4/standalone_webhook.go @@ -0,0 +1,134 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4 + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + enterprisev4 "github.com/splunk/splunk-operator/api/v4" + "github.com/splunk/splunk-operator/pkg/terms" +) + +// nolint:unused +// log is for logging in this package. +var standalonelog = logf.Log.WithName("standalone-resource") + +// SetupStandaloneWebhookWithManager registers the webhook for Standalone in the manager. +func SetupStandaloneWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr).For(&enterprisev4.Standalone{}). + WithValidator(&StandaloneCustomValidator{}). + WithDefaulter(&StandaloneCustomDefaulter{}). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +// +kubebuilder:webhook:path=/mutate-enterprise-splunk-com-v4-standalone,mutating=true,failurePolicy=fail,sideEffects=None,groups=enterprise.splunk.com,resources=standalones,verbs=create;update,versions=v4,name=mstandalone-v4.kb.io,admissionReviewVersions=v1 + +// StandaloneCustomDefaulter struct is responsible for setting default values on the custom resource of the +// Kind Standalone when those are created or updated. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as it is used only for temporary operations and does not need to be deeply copied. +type StandaloneCustomDefaulter struct { + // TODO(user): Add more fields as needed for defaulting +} + +var _ webhook.CustomDefaulter = &StandaloneCustomDefaulter{} + +// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Standalone. +func (d *StandaloneCustomDefaulter) Default(_ context.Context, obj runtime.Object) error { + standalone, ok := obj.(*enterprisev4.Standalone) + + if !ok { + return fmt.Errorf("expected an Standalone object but got %T", obj) + } + standalonelog.Info("Defaulting for Standalone", "name", standalone.GetName()) + + // TODO(user): fill in your defaulting logic. + + return nil +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here. +// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook. +// +kubebuilder:webhook:path=/validate-enterprise-splunk-com-v4-standalone,mutating=false,failurePolicy=fail,sideEffects=None,groups=enterprise.splunk.com,resources=standalones,verbs=create;update,versions=v4,name=vstandalone-v4.kb.io,admissionReviewVersions=v1 + +// StandaloneCustomValidator struct is responsible for validating the Standalone resource +// when it is created, updated, or deleted. +// +// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, +// as this struct is used only for temporary operations and does not need to be deeply copied. +type StandaloneCustomValidator struct { + // TODO(user): Add more fields as needed for validation +} + +var _ webhook.CustomValidator = &StandaloneCustomValidator{} + +// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Standalone. +func (v *StandaloneCustomValidator) ValidateCreate(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + standalone, ok := obj.(*enterprisev4.Standalone) + if !ok { + return nil, fmt.Errorf("expected a Standalone object but got %T", obj) + } + if !terms.Accepted() { + // Return a hard deny with a clear fix + return nil, fmt.Errorf( + "SPLUNK_GENERAL_TERMS not accepted. Set env var %q to %q on the Splunk Operator Deployment or Helm values, then retry the create", + terms.EnvVarName, terms.ExpectedFlag, + ) + } + standalonelog.Info("Validation for Standalone upon creation", "name", standalone.GetName()) + + // TODO(user): fill in your validation logic upon object creation. + + return nil, nil +} + +// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Standalone. +func (v *StandaloneCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + standalone, ok := newObj.(*enterprisev4.Standalone) + if !ok { + return nil, fmt.Errorf("expected a Standalone object for the newObj but got %T", newObj) + } + standalonelog.Info("Validation for Standalone upon update", "name", standalone.GetName()) + + // TODO(user): fill in your validation logic upon object update. + + return nil, nil +} + +// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Standalone. +func (v *StandaloneCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + standalone, ok := obj.(*enterprisev4.Standalone) + if !ok { + return nil, fmt.Errorf("expected a Standalone object but got %T", obj) + } + standalonelog.Info("Validation for Standalone upon deletion", "name", standalone.GetName()) + + // TODO(user): fill in your validation logic upon object deletion. + + return nil, nil +} diff --git a/internal/webhook/v4/standalone_webhook_test.go b/internal/webhook/v4/standalone_webhook_test.go new file mode 100644 index 000000000..4ef7f6de4 --- /dev/null +++ b/internal/webhook/v4/standalone_webhook_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + enterprisev4 "github.com/splunk/splunk-operator/api/v4" + // TODO (user): Add any additional imports if needed +) + +var _ = Describe("Standalone Webhook", func() { + var ( + obj *enterprisev4.Standalone + oldObj *enterprisev4.Standalone + validator StandaloneCustomValidator + defaulter StandaloneCustomDefaulter + ) + + BeforeEach(func() { + obj = &enterprisev4.Standalone{} + oldObj = &enterprisev4.Standalone{} + validator = StandaloneCustomValidator{} + Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") + defaulter = StandaloneCustomDefaulter{} + Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") + Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") + Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") + // TODO (user): Add any setup logic common to all tests + }) + + AfterEach(func() { + // TODO (user): Add any teardown logic common to all tests + }) + + Context("When creating Standalone under Defaulting Webhook", func() { + // TODO (user): Add logic for defaulting webhooks + // Example: + // It("Should apply defaults when a required field is empty", func() { + // By("simulating a scenario where defaults should be applied") + // obj.SomeFieldWithDefault = "" + // By("calling the Default method to apply defaults") + // defaulter.Default(ctx, obj) + // By("checking that the default values are set") + // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) + // }) + }) + + Context("When creating or updating Standalone under Validating Webhook", func() { + // TODO (user): Add logic for validating webhooks + // Example: + // It("Should deny creation if a required field is missing", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "" + // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) + // }) + // + // It("Should admit creation if all required fields are present", func() { + // By("simulating an invalid creation scenario") + // obj.SomeRequiredField = "valid_value" + // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) + // }) + // + // It("Should validate updates correctly", func() { + // By("simulating a valid update scenario") + // oldObj.SomeRequiredField = "updated_value" + // obj.SomeRequiredField = "updated_value" + // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) + // }) + }) + +}) diff --git a/internal/webhook/v4/webhook_suite_test.go b/internal/webhook/v4/webhook_suite_test.go new file mode 100644 index 000000000..ba5632b3e --- /dev/null +++ b/internal/webhook/v4/webhook_suite_test.go @@ -0,0 +1,164 @@ +/* +Copyright 2021. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "os" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + enterprisev4 "github.com/splunk/splunk-operator/api/v4" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var ( + ctx context.Context + cancel context.CancelFunc + k8sClient client.Client + cfg *rest.Config + testEnv *envtest.Environment +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + var err error + err = enterprisev4.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + // Retrieve the first found binary directory to allow running tests from IDEs + if getFirstFoundEnvTestBinaryDir() != "" { + testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() + } + + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager. + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = SetupStandaloneWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready. + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + + return conn.Close() + }).Should(Succeed()) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. +// ENVTEST-based tests depend on specific binaries, usually located in paths set by +// controller-runtime. When running tests directly (e.g., via an IDE) without using +// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. +// +// This function streamlines the process by finding the required binaries, similar to +// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are +// properly set up, run 'make setup-envtest' beforehand. +func getFirstFoundEnvTestBinaryDir() string { + basePath := filepath.Join("..", "..", "..", "bin", "k8s") + entries, err := os.ReadDir(basePath) + if err != nil { + logf.Log.Error(err, "Failed to read directory", "path", basePath) + return "" + } + for _, entry := range entries { + if entry.IsDir() { + return filepath.Join(basePath, entry.Name()) + } + } + return "" +} diff --git a/pkg/splunk/enterprise/standalone.go b/pkg/splunk/enterprise/standalone.go index 1c83eab5a..85beb3915 100644 --- a/pkg/splunk/enterprise/standalone.go +++ b/pkg/splunk/enterprise/standalone.go @@ -24,6 +24,7 @@ import ( enterpriseApi "github.com/splunk/splunk-operator/api/v4" splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" + "github.com/splunk/splunk-operator/pkg/tls" splctrl "github.com/splunk/splunk-operator/pkg/splunk/splkcontroller" splutil "github.com/splunk/splunk-operator/pkg/splunk/util" appsv1 "k8s.io/api/apps/v1" @@ -214,6 +215,8 @@ func ApplyStandalone(ctx context.Context, client splcommon.ControllerClient, cr return result, err } + _ = tls.ObserveAndUpdate(ctx, &standaloneStatusAdapter{c: client, cr: cr, sts: statefulSet}) + mgr := splctrl.DefaultStatefulSetPodManager{} phase, err := mgr.Update(ctx, client, statefulSet, cr.Spec.Replicas) cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas @@ -279,10 +282,15 @@ func getStandaloneStatefulSet(ctx context.Context, client splcommon.ControllerCl setupInitContainer(&ss.Spec.Template, cr.Spec.Image, cr.Spec.ImagePullPolicy, commandForStandaloneSmartstore, cr.Spec.CommonSplunkSpec.EtcVolumeStorageConfig.EphemeralStorage) } + err = mutateTLS(ctx, client, ss, cr) + if err != nil { + return nil, err + } + // Setup App framework staging volume for apps setupAppsStagingVolume(ctx, client, cr, &ss.Spec.Template, &cr.Spec.AppFrameworkConfig) - return ss, nil + return ss, err } // validateStandaloneSpec checks validity and makes default updates to a StandaloneSpec, and returns error if something is wrong. diff --git a/pkg/splunk/enterprise/standalone_status.go b/pkg/splunk/enterprise/standalone_status.go new file mode 100644 index 000000000..626ad79db --- /dev/null +++ b/pkg/splunk/enterprise/standalone_status.go @@ -0,0 +1,37 @@ +package enterprise + +import ( + "context" + + v4 "github.com/splunk/splunk-operator/api/v4" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type standaloneStatusAdapter struct { + c client.Client + cr *v4.Standalone + sts *appsv1.StatefulSet // or keep a copy of the PodTemplate annotations +} + +func (a *standaloneStatusAdapter) GetClient() client.Client { return a.c } +func (a *standaloneStatusAdapter) NamespacedName() types.NamespacedName { + return types.NamespacedName{Namespace: a.cr.Namespace, Name: a.cr.Name} +} +func (a *standaloneStatusAdapter) SpecTLS() *v4.TLSConfig { return a.cr.Spec.TLS } +func (a *standaloneStatusAdapter) ExistingTLSStatus() *v4.TLSStatus { return a.cr.Status.TLS } +func (a *standaloneStatusAdapter) UpdateTLSStatus(ctx context.Context, st *v4.TLSStatus) error { + a.cr.Status.TLS = st + //return a.c.Status().Update(ctx, a.cr) + return nil +} +func (a *standaloneStatusAdapter) PreTasksConfigMapName() string { + return a.cr.Name + "-tls-pre" +} +func (a *standaloneStatusAdapter) PodTemplateAnnotations() map[string]string { + if a.sts != nil && a.sts.Spec.Template.Annotations != nil { + return a.sts.Spec.Template.Annotations + } + return nil +} diff --git a/pkg/splunk/enterprise/tls_configuraiton.go b/pkg/splunk/enterprise/tls_configuraiton.go new file mode 100644 index 000000000..7adbee389 --- /dev/null +++ b/pkg/splunk/enterprise/tls_configuraiton.go @@ -0,0 +1,118 @@ +// pkg/splunk/enterprise/tls_configuration.go +package enterprise + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + "strings" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v4 "github.com/splunk/splunk-operator/api/v4" + splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" + tls "github.com/splunk/splunk-operator/pkg/tls" + "github.com/splunk/splunk-operator/pkg/splunk/splkcontroller" +) + +func mutateTLS(ctx context.Context, c splcommon.ControllerClient, ss *appsv1.StatefulSet, cr *v4.Standalone) error { + tlsSpec := cr.Spec.TLS + if tlsSpec == nil { + return nil + } + + preCMName := fmt.Sprintf("%s-tls-pretasks", cr.Name) + + // Optional: set [general] serverName; empty is fine + serverName := "" + + // KV password secret (optional) + var kvPassSel *corev1.SecretKeySelector + var kvPassFile string + if tlsSpec.KVEncryptedKey != nil && tlsSpec.KVEncryptedKey.Enabled && tlsSpec.KVEncryptedKey.PasswordSecretRef != nil { + kvPassSel = tlsSpec.KVEncryptedKey.PasswordSecretRef + if kvPassSel.Key != "" { + kvPassFile = tls.KVPassSrcMountDir + "/" + kvPassSel.Key + } + } + + // Best-effort checksum for CSI (Secret case is observed in status path) + observedTLSChecksum := "" + if tlsSpec.Provider == v4.TLSProviderCSI && tlsSpec.CSI != nil { + observedTLSChecksum = hashCSIAttrs(tlsSpec) + } + + // 1) Render pretasks + preYAML, preHash, err := tls.Render(tlsSpec, serverName, kvPassFile) + if err != nil { + return fmt.Errorf("render pretasks: %w", err) + } + + // 2) Create/Update the pretasks ConfigMap (vanilla metav1.ObjectMeta) + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: preCMName, + Namespace: cr.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "splunk-operator", + "app.kubernetes.io/name": "splunk", + "enterprise.splunk.com/owner": cr.Name, + }, + }, + Data: map[string]string{ + tls.PreTasksCMKey: preYAML, // usually "tls.yml" + }, + } + if _, err := splkcontroller.ApplyConfigMap(ctx, c, cm); err != nil { + return fmt.Errorf("apply pretasks configmap: %w", err) + } + + // 3) Inject volumes/mounts/env/annotations into the pod template + _, err = tls.InjectTLSForPodTemplate( + &ss.Spec.Template, + tlsSpec, + preCMName, // ConfigMap to mount at /mnt/pre/tls.yml + preHash, // annotation for pretask content hash + observedTLSChecksum, // annotation for TLS checksum (CSI best-effort) + kvPassSel, // optional KV pass Secret + ) + if err != nil { + return fmt.Errorf("inject tls into pod template: %w", err) + } + + return nil +} + +func hashCSIAttrs(cfg *v4.TLSConfig) string { + if cfg == nil || cfg.CSI == nil { + return "" + } + parts := []string{ + safe(cfg.CSI.IssuerRefKind), + safe(cfg.CSI.IssuerRefName), + strings.Join(sortedCopy(cfg.CSI.DNSNames), ","), + safe(cfg.CSI.Duration), + safe(cfg.CSI.RenewBefore), + safe(cfg.CSI.KeyAlgorithm), + fmt.Sprintf("%d", cfg.CSI.KeySize), + } + sum := sha256.Sum256([]byte(strings.Join(parts, "|"))) + return hex.EncodeToString(sum[:]) +} + +func sortedCopy(in []string) []string { + cp := append([]string(nil), in...) + sort.Strings(cp) + return cp +} + +func safe(s string) string { + if s == "" { + return "-" + } + return s +} diff --git a/pkg/terms/terms.go b/pkg/terms/terms.go new file mode 100644 index 000000000..d22e7f6c3 --- /dev/null +++ b/pkg/terms/terms.go @@ -0,0 +1,23 @@ +package terms + +import ( + "os" + "strings" + "sync/atomic" +) + +const ( + EnvVarName = "SPLUNK_GENERAL_TERMS" + ExpectedFlag = "--accept-sgt-current-at-splunk-com" +) + +var accepted atomic.Bool + +func InitFromEnv() { + got := strings.TrimSpace(os.Getenv(EnvVarName)) + accepted.Store(got == ExpectedFlag) +} + +func Accepted() bool { + return accepted.Load() +} \ No newline at end of file diff --git a/pkg/tls/constants.go b/pkg/tls/constants.go new file mode 100644 index 000000000..3853a895e --- /dev/null +++ b/pkg/tls/constants.go @@ -0,0 +1,41 @@ +package tls + +// Canonical locations inside the Splunk container +const ( + SplunkHomeDefault = "/opt/splunk" + CanonicalTLSDir = "/opt/splunk/etc/auth" // default; overridable by spec.tls.canonicalDir + //CanonicalTLSDir = "/opt/splunk/etc/auth" // default; overridable by spec.tls.canonicalDir + + // Source mounts + TLSSrcMountDir = "/mnt/certs" // Secret or CSI + TrustSrcMountDir = "/mnt/trust" // optional trust bundle Secret + KVPassSrcMountDir = "/mnt/kvpass"// optional secret for kv password + + // PRE-TASKS mount + PreTasksMountDir = "/mnt/pre" + PreTasksFilename = "tls.yml" // combined Go-templated playbook + PreTasksFileURI = "file:///mnt/pre/tls.yml" // SPLUNK_ANSIBLE_PRE_TASKS value + + // Canonical filenames under CanonicalTLSDir + TLSCrtName = "tls.crt" + TLSKeyName = "tls.key" + CACrtName = "ca.crt" + ServerPEMName = "server.pem" + TrustBundleName = "trust-bundle.crt" // where we copy an optional trust bundle + KVBundleDefaultName = "kvstore.pem" // if kvEncryptedKey.enabled + + // K8s resource names (volume/configmap keys) + PreTasksCMKey = "tls.yml" // key in ConfigMap data + + // Pod template annotations (visible diff even with OnDelete) + AnnTLSChecksum = "enterprise.splunk.com/tls-checksum" // sha256 of observed TLS inputs + AnnPreTasksChecksum = "enterprise.splunk.com/pretasks-checksum"// sha256 of rendered pretasks + + // Env + EnvPreTasks = "SPLUNK_ANSIBLE_PRE_TASKS" + EnvStartArgs = "SPLUNK_START_ARGS" +) + +// Small helper for int32 pointers +func Int32Ptr(v int32) *int32 { return &v } +func BoolPtr(b bool) *bool { return &b } diff --git a/pkg/tls/defaults.go b/pkg/tls/defaults.go new file mode 100644 index 000000000..0178dab91 --- /dev/null +++ b/pkg/tls/defaults.go @@ -0,0 +1,82 @@ +// pkg/tls/defaults.go +package tls + +import ( + "path/filepath" + + v4 "github.com/splunk/splunk-operator/api/v4" +) + +// Data is the input model for templates/pretasks.tmpl.yaml. +// NOTE: Go renders with custom delimiters [[ ... ]] so Ansible/Jinja {{ ... }} stays intact. +type Data struct { + // Core directories (resolved) + SplunkHome string // from SplunkHomeDefault + CanonicalDir string // spec override or CanonicalTLSDir + + // Mounted sources (resolved from constants; mutator mounts them) + SrcDir string // TLSSrcMountDir + TrustDir string // TrustSrcMountDir + TrustKey string // key name inside the trust-bundle Secret (default "ca-bundle.crt") + + // Canonical destination file paths (under CanonicalDir) + TLSCrt string // CanonicalDir/TLSCrtName + TLSKey string // CanonicalDir/TLSKeyName + CACrt string // CanonicalDir/CACrtName + ServerPEM string // CanonicalDir/ServerPEMName + CABundle string // CanonicalDir/TrustBundleName + + // KV encrypted key/bundle (optional) + KVEnable bool // spec.KVEncryptedKey.Enabled + KVBundlePath string // CanonicalDir/ + KVPasswordFile string // set by mutator if a Secret is mounted (may be empty) + + // Optional serverName for [general]; the caller sets if desired + ServerName string +} + +// defaultsFor applies operator defaults using constants from constants.go. +func defaultsFor(spec *v4.TLSConfig) Data { + // Trust-manager Secret default key (source). Destination file name is TrustBundleName. + const defaultTrustSecretKey = "ca-bundle.crt" + + d := Data{ + SplunkHome: SplunkHomeDefault, + SrcDir: TLSSrcMountDir, + TrustDir: TrustSrcMountDir, + TrustKey: defaultTrustSecretKey, + } + + // CanonicalDir (spec override) + canon := spec.CanonicalDir + if canon == "" { + canon = CanonicalTLSDir + } + d.CanonicalDir = canon + + // Canonical file paths + d.TLSCrt = filepath.Join(canon, TLSCrtName) + d.TLSKey = filepath.Join(canon, TLSKeyName) + d.CACrt = filepath.Join(canon, CACrtName) + d.ServerPEM = filepath.Join(canon, ServerPEMName) + d.CABundle = filepath.Join(canon, TrustBundleName) + + // Trust bundle Secret key override (source key), if provided + if spec.TrustBundle != nil && spec.TrustBundle.Key != "" { + d.TrustKey = spec.TrustBundle.Key + } + + // KV encrypted key settings + if spec.KVEncryptedKey != nil && spec.KVEncryptedKey.Enabled { + d.KVEnable = true + bundleFile := KVBundleDefaultName + if bf := spec.KVEncryptedKey.BundleFile; bf != "" { + bundleFile = bf + } + d.KVBundlePath = filepath.Join(canon, bundleFile) + // d.KVPasswordFile is set by the mutator if you mount a password Secret at KVPassSrcMountDir. + } + + // d.ServerName (and d.KVPasswordFile) are set by the caller (renderer/mutator) as needed. + return d +} diff --git a/pkg/tls/mutate.go b/pkg/tls/mutate.go new file mode 100644 index 000000000..dbda298cb --- /dev/null +++ b/pkg/tls/mutate.go @@ -0,0 +1,232 @@ +package tls + +import ( + "fmt" + "sort" + "strings" + + corev1 "k8s.io/api/core/v1" + + v4 "github.com/splunk/splunk-operator/api/v4" +) + +// InjectTLSForPodTemplate wires TLS mounts/env/annotations into the PodTemplate. +// +// It expects that the reconciler already: +// * created/updated a ConfigMap named preTasksCMName with key splunk.PreTasksCMKey holding the rendered pretasks YAML +// * computed observedTLSChecksum (e.g., sha256 of Secret data or CSI attributes) +// * computed preTasksHash (sha256 of rendered pretasks content) +// +// This function: +// * mounts the pretasks CM at /mnt/pre/tls.yml and sets SPLUNK_ANSIBLE_PRE_TASKS +// * mounts Secret or CSI at /mnt/certs +// * mounts optional trust-bundle Secret at /mnt/trust +// * mounts optional KV password Secret at /mnt/kvpass (and returns its file path for renderer convenience) +// * annotates the pod template with TLS and pretask checksums (visible diff with OnDelete strategy) +func InjectTLSForPodTemplate( + podTemplate *corev1.PodTemplateSpec, + spec *v4.TLSConfig, + preTasksCMName string, + preTasksHash string, + observedTLSChecksum string, + kvPassMount *corev1.SecretKeySelector, // optional (used only when KVEncryptedKey.enabled) +) (kvPassFile string, err error) { + if podTemplate == nil { + return "", fmt.Errorf("podTemplate is nil") + } + if spec == nil { + return "", fmt.Errorf("tls spec is nil") + } + if len(podTemplate.Spec.Containers) == 0 { + return "", fmt.Errorf("podTemplate has no containers") + } + + // ----------------------- + // 1) PRE-TASKS CM mount + // ----------------------- + addVol(podTemplate, corev1.Volume{ + Name: "pre", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: preTasksCMName}, + Items: []corev1.KeyToPath{{ + Key: PreTasksCMKey, + Path: PreTasksFilename, + Mode: Int32Ptr(0444), + }}, + DefaultMode: Int32Ptr(0444), + }, + }, + }) + addMnt(podTemplate, corev1.VolumeMount{Name: "pre", MountPath: PreTasksMountDir, ReadOnly: true}) + + // ---------------------------------------------------- + // 2) TLS source (Secret or CSI) -> /mnt/certs + // ---------------------------------------------------- + switch spec.Provider { + case v4.TLSProviderSecret: + if spec.SecretRef == nil || spec.SecretRef.Name == "" { + return "", fmt.Errorf("tls.provider=Secret but secretRef is empty") + } + addVol(podTemplate, corev1.Volume{ + Name: "tls-src", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: spec.SecretRef.Name, + DefaultMode: Int32Ptr(0400), + }, + }, + }) + addMnt(podTemplate, corev1.VolumeMount{Name: "tls-src", MountPath: TLSSrcMountDir, ReadOnly: true}) + + case v4.TLSProviderCSI: + if spec.CSI == nil { + return "", fmt.Errorf("tls.provider=CSI but csi is nil") + } + attrs := map[string]string{ + "csi.storage.k8s.io/ephemeral": "true", + "issuer-name": spec.CSI.IssuerRefName, + "issuer-kind": spec.CSI.IssuerRefKind, + "issuer-group": "cert-manager.io", + } + if len(spec.CSI.DNSNames) > 0 { + attrs["dns-names"] = joinCSV(spec.CSI.DNSNames) + } + if spec.CSI.Duration != "" { + attrs["duration"] = spec.CSI.Duration + } + if spec.CSI.RenewBefore != "" { + attrs["renew-before"] = spec.CSI.RenewBefore + } + if spec.CSI.KeyAlgorithm != "" { + attrs["key-algorithm"] = spec.CSI.KeyAlgorithm + } + if spec.CSI.KeySize > 0 { + attrs["key-size"] = fmt.Sprintf("%d", spec.CSI.KeySize) + } + + addVol(podTemplate, corev1.Volume{ + Name: "tls-src", + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + Driver: "csi.cert-manager.io", + ReadOnly: BoolPtr(true), + VolumeAttributes: attrs, + }, + }, + }) + addMnt(podTemplate, corev1.VolumeMount{Name: "tls-src", MountPath: TLSSrcMountDir, ReadOnly: true}) + + default: + return "", fmt.Errorf("unsupported tls.provider: %q", string(spec.Provider)) + } + + // --------------------------------------------- + // 3) Optional trust-bundle Secret -> /mnt/trust + // --------------------------------------------- + if spec.TrustBundle != nil && spec.TrustBundle.SecretName != "" { + addVol(podTemplate, corev1.Volume{ + Name: "trust-src", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: spec.TrustBundle.SecretName, + DefaultMode: Int32Ptr(0444), + }, + }, + }) + addMnt(podTemplate, corev1.VolumeMount{Name: "trust-src", MountPath: TrustSrcMountDir, ReadOnly: true}) + } + + // -------------------------------------------------------- + // 4) Optional KV password Secret -> /mnt/kvpass/ + // -------------------------------------------------------- + if spec.KVEncryptedKey != nil && spec.KVEncryptedKey.Enabled && kvPassMount != nil { + if kvPassMount.Name != "" { + addVol(podTemplate, corev1.Volume{ + Name: "kvpass", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: kvPassMount.Name, + DefaultMode: Int32Ptr(0400), + }, + }, + }) + addMnt(podTemplate, corev1.VolumeMount{Name: "kvpass", MountPath: KVPassSrcMountDir, ReadOnly: true}) + if kvPassMount.Key != "" { + kvPassFile = KVPassSrcMountDir + "/" + kvPassMount.Key + } + } + } + + // -------------------------------------------------------- + // 5) Env on Splunk container + // -------------------------------------------------------- + c := &podTemplate.Spec.Containers[0] + c.Env = upsertEnv(c.Env, corev1.EnvVar{Name: EnvPreTasks, Value: PreTasksFileURI}) + // After InjectTLSForPodTemplate(...) + if len(podTemplate.Spec.Containers) == 0 { + return kvPassFile, fmt.Errorf("no containers in pod template") + } + spl := &podTemplate.Spec.Containers[0] + add := func(n, v string) { + found := false + for i := range spl.Env { if spl.Env[i].Name == n { spl.Env[i].Value = v; found = true; break } } + if !found { spl.Env = append(spl.Env, corev1.EnvVar{Name: n, Value: v}) } + } + + // Web SSL (keeps Ansible roles consistent with our file layout) + add("SPLUNK_HTTP_ENABLESSL", "1") + add("SPLUNK_HTTP_ENABLESSL_CERT", fmt.Sprintf("%s/%s", CanonicalTLSDir, TLSCrtName)) + add("SPLUNK_HTTP_ENABLESSL_PRIVKEY", fmt.Sprintf("%s/%s", CanonicalTLSDir, TLSKeyName)) + add("SPLUNK_HTTP_ENABLESSL_PRIVKEY_PASSWORD", "") + + // splunkd SSL (we point to our combined server.pem and CA) + add("SPLUNKD_SSL_ENABLE", "true") + add("SPLUNKD_SSL_CERT", fmt.Sprintf("%s/%s", CanonicalTLSDir, ServerPEMName)) + add("SPLUNKD_SSL_CA", fmt.Sprintf("%s/%s", CanonicalTLSDir, CACrtName)) + + // -------------------------------------------------------- + // 6) Annotations for visibility (even with OnDelete) + // -------------------------------------------------------- + if podTemplate.Annotations == nil { + podTemplate.Annotations = map[string]string{} + } + podTemplate.Annotations[AnnTLSChecksum] = safeTrunc(observedTLSChecksum) + podTemplate.Annotations[AnnPreTasksChecksum] = safeTrunc(preTasksHash) + + return kvPassFile, nil +} + +// --- helpers --- + +func addVol(pt *corev1.PodTemplateSpec, v corev1.Volume) { + pt.Spec.Volumes = append(pt.Spec.Volumes, v) +} + +func addMnt(pt *corev1.PodTemplateSpec, m corev1.VolumeMount) { + c := &pt.Spec.Containers[0] + c.VolumeMounts = append(c.VolumeMounts, m) +} + +func upsertEnv(env []corev1.EnvVar, nv corev1.EnvVar) []corev1.EnvVar { + for i := range env { + if env[i].Name == nv.Name { + env[i] = nv + return env + } + } + return append(env, nv) +} + +func joinCSV(ss []string) string { + cp := append([]string(nil), ss...) + sort.Strings(cp) + return strings.Join(cp, ",") +} + +func safeTrunc(s string) string { + if len(s) <= 64 { + return s + } + return s[:64] +} diff --git a/pkg/tls/pretasks_renderer.go b/pkg/tls/pretasks_renderer.go new file mode 100644 index 000000000..7561dd53a --- /dev/null +++ b/pkg/tls/pretasks_renderer.go @@ -0,0 +1,63 @@ +// pkg/tls/pretasks_renderer.go +package tls + +import ( + "crypto/sha256" + "embed" + "encoding/hex" + "fmt" + "io/fs" + "strings" + "text/template" + + v4 "github.com/splunk/splunk-operator/api/v4" +) + +// Embed the default pretasks template. This file must exist at build time. +// Keep this path aligned with your repo: pkg/tls/templates/pretasks.tmpl.yaml + +//go:embed templates/pretasks.tmpl.yaml +var embeddedPreTasksFS embed.FS + +// indirection used by tests to point at a temp FS + path +var ( + preTasksFS fs.FS = embeddedPreTasksFS + preTasksPath string = "templates/pretasks.tmpl.yaml" +) + +// Render renders the pretasks Ansible YAML from the TLS spec and returns the +// YAML plus a sha256 hex digest of the rendered content. +func Render(spec *v4.TLSConfig, serverName, kvPasswordFile string) (string, string, error) { + if spec == nil { + spec = &v4.TLSConfig{} + } + + // Resolve defaults and fill dynamic fields + d := defaultsFor(spec) + d.ServerName = serverName + d.KVPasswordFile = kvPasswordFile + + // Load template from the (overridable) FS + tplBytes, err := fs.ReadFile(preTasksFS, preTasksPath) + if err != nil { + return "", "", fmt.Errorf("read %s: %w", preTasksPath, err) + } + + // IMPORTANT: change Go template delimiters so Ansible/Jinja {{ ... }} is untouched. + // Also fail fast on missing keys to catch template/data drift in CI. + tpl, err := template.New("pretasks"). + Delims("[[", "]]"). + Option("missingkey=error"). + Parse(string(tplBytes)) + if err != nil { + return "", "", fmt.Errorf("parse pretasks template: %w", err) + } + + var b strings.Builder + if err := tpl.Execute(&b, d); err != nil { + return "", "", fmt.Errorf("execute pretasks template: %w", err) + } + + sum := sha256.Sum256([]byte(b.String())) + return b.String(), hex.EncodeToString(sum[:]), nil +} diff --git a/pkg/tls/pretasks_renderer_template_test.go b/pkg/tls/pretasks_renderer_template_test.go new file mode 100644 index 000000000..0cef58a24 --- /dev/null +++ b/pkg/tls/pretasks_renderer_template_test.go @@ -0,0 +1,204 @@ +package tls + +import ( + "os" + "path/filepath" + "strings" + "testing" + + v4 "github.com/splunk/splunk-operator/api/v4" + "gopkg.in/yaml.v3" +) + +// --- helpers --- + +// render renders the real embedded template with options for KV and serverName. +func render(t *testing.T, kv bool, serverName string) string { + t.Helper() + spec := &v4.TLSConfig{ + CanonicalDir: "/opt/splunk/etc/auth/tls", + } + if kv { + spec.KVEncryptedKey = &v4.KVEncryptedKeySpec{Enabled: true} + } + yml, _, err := Render(spec, serverName, "/mnt/kvpass/pass") + if err != nil { + t.Fatalf("Render failed: %v", err) + } + return yml +} + +// dumpRendered writes the rendered YAML to a temp file and logs the path. +// Use `go test -v` to see the path and the YAML inline. +func dumpRendered(t *testing.T, name, yml string) string { + t.Helper() + dir := t.TempDir() + p := filepath.Join(dir, name) + if err := os.WriteFile(p, []byte(yml), 0o644); err != nil { + t.Fatalf("write %s: %v", p, err) + } + t.Logf("\n--- RENDERED pretasks (%s) ---\n%s\n--- END RENDERED ---\n(saved to %s)", name, yml, p) + return p +} + +func requireNoJinja(t *testing.T, s string) { + t.Helper() + if strings.Contains(s, "{{") || strings.Contains(s, "{%") || strings.Contains(s, "{#") { + t.Fatalf("Found Jinja remnants in rendered output") + } +} + +// Optional: ensure no leftover Go actions remain. +// (Our renderer should replace everything; this is a sanity check.) +func requireNoGoDelims(t *testing.T, s string) { + t.Helper() + if strings.Contains(s, "[[") || strings.Contains(s, "]]") { + t.Fatalf("Found leftover Go template delimiters in output") + } +} + +// YAML shape we expect at the top level - a list of task maps. +type yamlRoot = []map[string]any + +func parseYAML(t *testing.T, s string) yamlRoot { + t.Helper() + var root yamlRoot + if err := yaml.Unmarshal([]byte(s), &root); err != nil { + t.Fatalf("Rendered YAML failed to parse: %v", err) + } + return root +} + +// Walk all tasks, including tasks in a "block:" list, and run visit on each. +func walkTasks(tasks yamlRoot, visit func(task map[string]any)) { + var walk func(any) + walk = func(x any) { + switch v := x.(type) { + case []any: + for _, e := range v { + walk(e) + } + case []map[string]any: + for _, m := range v { + walk(m) + } + case map[string]any: + visit(v) + // descend into "block" if present + if b, ok := v["block"]; ok { + walk(b) + } + } + } + for _, t := range tasks { + walk(t) + } +} + +// Check structure for ini_file tasks: +// - If a task has "ini_file", ensure task-level "no_extra_spaces" is NOT present. +// - If "no_extra_spaces" exists under "ini_file", it must be a boolean. +func assertIniFileNoExtraSpacesPlacement(t *testing.T, tasks yamlRoot) { + t.Helper() + walkTasks(tasks, func(task map[string]any) { + name, _ := task["name"].(string) + ini, hasIni := task["ini_file"] + if !hasIni { + // still ensure we did not accidentally put task-level no_extra_spaces anywhere + if _, bad := task["no_extra_spaces"]; bad { + t.Fatalf("task %q has no_extra_spaces at task root, but has no ini_file module", name) + } + return + } + // ini_file must be a map + iniMap, ok := ini.(map[string]any) + if !ok { + t.Fatalf("task %q ini_file is not a mapping", name) + } + // task-level no_extra_spaces is invalid when ini_file is present + if _, bad := task["no_extra_spaces"]; bad { + t.Fatalf("task %q has no_extra_spaces at task root, it must be under ini_file", name) + } + // if nested, must be boolean + if v, present := iniMap["no_extra_spaces"]; present { + if _, isBool := v.(bool); !isBool { + t.Fatalf("task %q ini_file.no_extra_spaces must be boolean, got %#v", name, v) + } + } + }) +} + +// --- tests --- + +// Renders the real embedded template, ensures no Jinja, parses as YAML, +// checks ini_file.no_extra_spaces is nested correctly, and logs the final playbook. +func TestTemplate_Real_YAML_NoJinja_BalancedAndStructured(t *testing.T) { + out := render(t, false, "host.example.com") + dumpRendered(t, "pretasks.rendered.yaml", out) + + requireNoJinja(t, out) + // Optional: if you want to be strict that no [[...]] remain at all: + requireNoGoDelims(t, out) + + root := parseYAML(t, out) + assertIniFileNoExtraSpacesPlacement(t, root) +} + +// Ensures the serverName stanza is only present when serverName is non-empty. +func TestTemplate_ServerName_Guard(t *testing.T) { + // serverName empty -> no ini_file option=serverName + outEmpty := render(t, false, "") + dumpRendered(t, "pretasks.servername-empty.yaml", outEmpty) + rootEmpty := parseYAML(t, outEmpty) + + found := false + walkTasks(rootEmpty, func(task map[string]any) { + if ini, ok := task["ini_file"].(map[string]any); ok { + if opt, _ := ini["option"].(string); opt == "serverName" { + found = true + } + } + }) + if found { + t.Fatalf("serverName ini_file task present when ServerName is empty") + } + + // serverName provided -> must exist and be set to value + const want = "splunk.example.com" + outWith := render(t, false, want) + dumpRendered(t, "pretasks.servername-present.yaml", outWith) + rootWith := parseYAML(t, outWith) + + found = false + walkTasks(rootWith, func(task map[string]any) { + if ini, ok := task["ini_file"].(map[string]any); ok { + if opt, _ := ini["option"].(string); opt == "serverName" { + if val, _ := ini["value"].(string); val == want { + found = true + } + } + } + }) + if !found { + t.Fatalf("serverName ini_file task missing or incorrect when ServerName is provided") + } +} + +// Ensures KV tasks appear only when KVEnable is true. +func TestTemplate_KV_Guard(t *testing.T) { + outNoKV := render(t, false, "") + dumpRendered(t, "pretasks.kv-disabled.yaml", outNoKV) + if strings.Contains(outNoKV, "KV: build encrypted bundle") || + strings.Contains(outNoKV, "/opt/splunk/auth/kvstore.pem") || + strings.Contains(outNoKV, "tls_enc.key") { + t.Fatalf("KV tasks found when KVEnable=false") + } + + outKV := render(t, true, "") + dumpRendered(t, "pretasks.kv-enabled.yaml", outKV) + if !strings.Contains(outKV, "KV: build encrypted bundle") || + !strings.Contains(outKV, "/opt/splunk/auth/kvstore.pem") || + !strings.Contains(outKV, "tls_enc.key") { + t.Fatalf("KV tasks missing when KVEnable=true") + } +} diff --git a/pkg/tls/pretasks_renderer_test.go b/pkg/tls/pretasks_renderer_test.go new file mode 100644 index 000000000..ac6ea0c72 --- /dev/null +++ b/pkg/tls/pretasks_renderer_test.go @@ -0,0 +1,143 @@ +package tls + +import ( + "os" + "path/filepath" + "strings" + "testing" + + v4 "github.com/splunk/splunk-operator/api/v4" +) + +// helper to make a temporary template FS with given contents +func withTempTemplate(t *testing.T, content string, fn func()) { + t.Helper() + dir := t.TempDir() + templatesDir := filepath.Join(dir, "templates") + if err := os.MkdirAll(templatesDir, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + p := filepath.Join(templatesDir, "pretasks.tmpl.yaml") + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + + // override the package-level indirection vars + oldFS, oldPath := preTasksFS, preTasksPath + preTasksFS, preTasksPath = os.DirFS(dir), "templates/pretasks.tmpl.yaml" + defer func() { preTasksFS, preTasksPath = oldFS, oldPath }() + + fn() +} + +const goodTemplate = `--- +- name: Ensure canonical TLS dir + file: + path: "[[ .CanonicalDir ]]" + state: directory + owner: splunk + group: splunk + mode: "0755" + +- name: Minimal check - assert exists + assert: + that: + - '[[ .SplunkHome ]]' != '' + - '[[ .CanonicalDir ]]' != '' + success_msg: "Baseline ok" + +[[ if ne .ServerName "" ]] +- name: Optional serverName + debug: { msg: "serverName=[[ .ServerName ]]" } +[[ end ]] + +[[ if .KVEnable ]] +- name: KV enabled section marker + debug: { msg: "KV bundle at [[ .KVBundlePath ]]" } +[[ end ]] +` + +const badTemplate = `--- +- name: Broken template + debug: { msg: "[[ .CanonicalDir ]" } # missing closing ]] +` + +const missingKeyTpl = `--- +- name: Reference a missing field + debug: { msg: "[[ .DoesNotExist ]]" } +` + +func TestRender_Success_WithAndWithoutKV(t *testing.T) { + withTempTemplate(t, goodTemplate, func() { + // Case 1: KV disabled, no serverName + spec := &v4.TLSConfig{ + CanonicalDir: "/opt/splunk/etc/auth/tls", + // KVEncryptedKey is nil, so KVEnable=false + } + yml, sum, err := Render(spec, "", "") + if err != nil { + t.Fatalf("Render error (KV disabled): %v", err) + } + if len(strings.TrimSpace(yml)) == 0 { + t.Fatal("empty YAML output") + } + if len(sum) != 64 { + t.Fatalf("sha256 hex length = %d, want 64", len(sum)) + } + if strings.Contains(yml, "serverName=") { + t.Errorf("did not expect serverName section when empty, got:\n%s", yml) + } + if strings.Contains(yml, "KV enabled section marker") { + t.Errorf("did not expect KV section when disabled") + } + + // Case 2: KV enabled, with serverName + spec2 := &v4.TLSConfig{ + CanonicalDir: "/opt/splunk/etc/auth/tls", + KVEncryptedKey: &v4.KVEncryptedKeySpec{ + Enabled: true, + BundleFile: "", // default path from renderer + }, + } + yml2, sum2, err := Render(spec2, "splunk.example.com", "/mnt/kvpass/pass") + if err != nil { + t.Fatalf("Render error (KV enabled): %v", err) + } + if !strings.Contains(yml2, "serverName=splunk.example.com") { + t.Errorf("expected serverName stanza when provided, got:\n%s", yml2) + } + if !strings.Contains(yml2, "KV enabled section marker") { + t.Errorf("expected KV marker section when KVEnable=true, got:\n%s", yml2) + } + if sum2 == "" || len(sum2) != 64 { + t.Fatalf("unexpected hash: %q", sum2) + } + }) +} + +func TestRender_Failure_ParseError(t *testing.T) { + withTempTemplate(t, badTemplate, func() { + spec := &v4.TLSConfig{CanonicalDir: "/opt/splunk/etc/auth/tls"} + _, _, err := Render(spec, "", "") + if err == nil { + t.Fatal("expected parse error, got nil") + } + if !strings.Contains(err.Error(), "parse pretasks template") { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestRender_Failure_MissingKey(t *testing.T) { + withTempTemplate(t, missingKeyTpl, func() { + spec := &v4.TLSConfig{CanonicalDir: "/opt/splunk/etc/auth/tls"} + _, _, err := Render(spec, "", "") + if err == nil { + t.Fatal("expected missingkey error, got nil") + } + // produced from Execute with Option("missingkey=error") + if !strings.Contains(err.Error(), "execute pretasks template") { + t.Fatalf("unexpected error: %v", err) + } + }) +} diff --git a/pkg/tls/status.go b/pkg/tls/status.go new file mode 100644 index 000000000..54e50ca7e --- /dev/null +++ b/pkg/tls/status.go @@ -0,0 +1,311 @@ +package tls + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "fmt" + "sort" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + v4 "github.com/splunk/splunk-operator/api/v4" +) + +// StatusUpdater is implemented by each CR controller (Standalone, SHC, etc.). +// Keep this thin and easy to implement. +type StatusUpdater interface { + GetClient() client.Client + NamespacedName() types.NamespacedName + + SpecTLS() *v4.TLSConfig + ExistingTLSStatus() *v4.TLSStatus + UpdateTLSStatus(ctx context.Context, st *v4.TLSStatus) error + + // For PRE_TASKS hashing and surfacing what is really applied + PreTasksConfigMapName() string // e.g. -tls-pre + PodTemplateAnnotations() map[string]string // from the current PodTemplate +} + +// ObserveAndUpdate computes current TLS state and writes Status if changed. +// Does not block reconcile; callers may ignore errors. +func ObserveAndUpdate(ctx context.Context, u StatusUpdater) error { + cfg := u.SpecTLS() + if cfg == nil { + return nil + } + now := metav1.NewTime(time.Now().UTC()) + + st := &v4.TLSStatus{ + Provider: cfg.Provider, + CanonicalDir: firstNonEmpty(cfg.CanonicalDir, CanonicalTLSDir), + Conditions: []v4.TLSCondition{}, + } + + switch cfg.Provider { + case v4.TLSProviderSecret: + obs, bundleHash, conds, err := observeFromSecret(ctx, u.GetClient(), u.NamespacedName().Namespace, cfg) + if err != nil { + conds = append(conds, condition(v4.TLSReady, corev1.ConditionFalse, "SecretReadError", err.Error())) + } + st.Observed = obs + st.TrustBundleHash = bundleHash + st.Conditions = append(st.Conditions, conds...) + + case v4.TLSProviderCSI: + // Best-effort fingerprint from CSI attributes + obs := v4.TLSObserved{ + Source: fmt.Sprintf("CSI:%s/%s", safe(cfg.CSI.IssuerRefKind), safe(cfg.CSI.IssuerRefName)), + Hash: hashCSIAttributes(cfg), + } + st.Observed = obs + + // Optional trust bundle remains observable + if cfg.TrustBundle != nil && cfg.TrustBundle.SecretName != "" { + if h, err := hashSingleKeySecret(ctx, u.GetClient(), u.NamespacedName().Namespace, cfg.TrustBundle.SecretName, firstNonEmpty(cfg.TrustBundle.Key, "ca-bundle.crt")); err == nil && h != "" { + st.TrustBundleHash = h + st.Conditions = append(st.Conditions, condition(v4.TLSTrustBundleReady, corev1.ConditionTrue, "Observed", "Trust bundle Secret observed")) + } + } + st.Conditions = append(st.Conditions, condition(v4.TLSTrackingLimited, corev1.ConditionTrue, "CSIContentNotObservable", "Using best-effort hash from CSI attributes")) + + default: + // Unknown provider; leave status minimal but not failing the controller + st.Conditions = append(st.Conditions, condition(v4.TLSReady, corev1.ConditionFalse, "UnsupportedProvider", string(cfg.Provider))) + } + + // Hash the PRE_TASKS content we apply - based on the live ConfigMap + if cmHash, err := hashPreTasksConfigMap(ctx, u); err == nil && cmHash != "" { + st.PreTasksHash = cmHash + } + + // Surface current PodTemplate TLS checksum (annotation is set during mutate) + if ann := u.PodTemplateAnnotations(); ann != nil { + if v, ok := ann[AnnTLSChecksum]; ok { + st.PodTemplateChecksum = v + } + } + + // Ready when we have any observed fingerprint + if st.Observed.Hash != "" { + st.Conditions = upsert(st.Conditions, condition(v4.TLSReady, corev1.ConditionTrue, "Observed", "TLS material observed")) + } + st.LastObserved = now + + // Rotation signal - compare with previous + prev := u.ExistingTLSStatus() + if rotationNeeded(prev, st) { + st.Conditions = upsert(st.Conditions, condition(v4.TLSRotatePending, corev1.ConditionTrue, "MaterialChanged", "TLS material changed, restart required for OnDelete")) + } else { + st.Conditions = upsert(st.Conditions, condition(v4.TLSRotatePending, corev1.ConditionFalse, "NoChange", "No TLS material change detected")) + } + + // Persist if any change + if !tlsStatusEqual(prev, st) { + return u.UpdateTLSStatus(ctx, st) + } + return nil +} + +func observeFromSecret(ctx context.Context, c client.Client, ns string, cfg *v4.TLSConfig) (v4.TLSObserved, string, []v4.TLSCondition, error) { + var conds []v4.TLSCondition + obs := v4.TLSObserved{} + + if cfg.SecretRef == nil || cfg.SecretRef.Name == "" { + return obs, "", conds, fmt.Errorf("secretRef.name required for provider=Secret") + } + + sec := &corev1.Secret{} + if err := c.Get(ctx, types.NamespacedName{Namespace: ns, Name: cfg.SecretRef.Name}, sec); err != nil { + return obs, "", conds, err + } + obs.Source = "Secret/" + sec.Name + obs.SecretResourceVersion = sec.ResourceVersion + + // Hash over presence + bytes of tls.crt, tls.key, ca.crt + keys := []string{"tls.crt", "tls.key", "ca.crt"} + h := sha256.New() + for _, k := range keys { + if b, ok := sec.Data[k]; ok { + h.Write([]byte(k + ":")) + h.Write(b) + } else { + h.Write([]byte(k + ":")) + } + } + obs.Hash = hex.EncodeToString(h.Sum(nil)) + + // Parse leaf certificate best-effort + if crtBytes, ok := sec.Data["tls.crt"]; ok && len(crtBytes) > 0 { + if leaf := parseFirstCert(crtBytes); leaf != nil { + lh := sha256.Sum256(leaf.Raw) + obs.LeafSHA256 = hex.EncodeToString(lh[:]) + nb := metav1.NewTime(leaf.NotBefore.UTC()) + na := metav1.NewTime(leaf.NotAfter.UTC()) + obs.NotBefore, obs.NotAfter = &nb, &na + obs.SerialNumber = leaf.SerialNumber.String() + conds = append(conds, condition(v4.TLSReady, corev1.ConditionTrue, "Observed", "Secret with tls.crt parsed")) + } + } + + // Trust bundle hash if configured + bundleHash := "" + if cfg.TrustBundle != nil && cfg.TrustBundle.SecretName != "" { + if h, err := hashSingleKeySecret(ctx, c, ns, cfg.TrustBundle.SecretName, firstNonEmpty(cfg.TrustBundle.Key, "ca-bundle.crt")); err == nil && h != "" { + bundleHash = h + conds = append(conds, condition(v4.TLSTrustBundleReady, corev1.ConditionTrue, "Observed", "Trust bundle Secret observed")) + } + } + + return obs, bundleHash, conds, nil +} + +// hashPreTasksConfigMap returns the sha256 of the live pretasks ConfigMap data (tls.yml). +func hashPreTasksConfigMap(ctx context.Context, u StatusUpdater) (string, error) { + name := u.PreTasksConfigMapName() + if name == "" { + return "", nil + } + cm := &corev1.ConfigMap{} + if err := u.GetClient().Get(ctx, types.NamespacedName{Namespace: u.NamespacedName().Namespace, Name: name}, cm); err != nil { + return "", nil // not fatal; just means we can't surface the hash yet + } + if s, ok := cm.Data[PreTasksCMKey]; ok && s != "" { + sum := sha256.Sum256([]byte(s)) + return hex.EncodeToString(sum[:]), nil + } + return "", nil +} + +func hashSingleKeySecret(ctx context.Context, c client.Client, ns, name, key string) (string, error) { + sec := &corev1.Secret{} + if err := c.Get(ctx, types.NamespacedName{Namespace: ns, Name: name}, sec); err != nil { + return "", err + } + b, ok := sec.Data[key] + if !ok { + return "", fmt.Errorf("key %q missing", key) + } + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]), nil +} + +func hashCSIAttributes(cfg *v4.TLSConfig) string { + if cfg == nil || cfg.CSI == nil { + return "" + } + parts := []string{ + safe(cfg.CSI.IssuerRefKind), + safe(cfg.CSI.IssuerRefName), + strings.Join(append([]string{}, cfg.CSI.DNSNames...), ","), + safe(cfg.CSI.Duration), + safe(cfg.CSI.RenewBefore), + safe(cfg.CSI.KeyAlgorithm), + fmt.Sprintf("%d", cfg.CSI.KeySize), + } + sum := sha256.Sum256([]byte(strings.Join(parts, "|"))) + return hex.EncodeToString(sum[:]) +} + +func rotationNeeded(prev, cur *v4.TLSStatus) bool { + if prev == nil { + return false + } + if prev.Observed.Hash != cur.Observed.Hash { + return true + } + if prev.TrustBundleHash != cur.TrustBundleHash { + return true + } + if prev.PreTasksHash != cur.PreTasksHash { + return true + } + // PodTemplateChecksum is informational; do not use it to trigger rotation. + return false +} + +func tlsStatusEqual(a, b *v4.TLSStatus) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Provider == b.Provider && + a.CanonicalDir == b.CanonicalDir && + a.Observed.Hash == b.Observed.Hash && + a.Observed.SecretResourceVersion == b.Observed.SecretResourceVersion && + a.TrustBundleHash == b.TrustBundleHash && + a.PreTasksHash == b.PreTasksHash && + a.PodTemplateChecksum == b.PodTemplateChecksum +} + +func condition(t v4.TLSConditionType, s corev1.ConditionStatus, reason, msg string) v4.TLSCondition { + return v4.TLSCondition{ + Type: t, + Status: s, + Reason: reason, + Message: msg, + LastTransitionTime: metav1.Now(), + } +} + +func upsert(list []v4.TLSCondition, c v4.TLSCondition) []v4.TLSCondition { + // replace if same Type exists + for i := range list { + if list[i].Type == c.Type { + list[i] = c + sort.Slice(list, func(i, j int) bool { return list[i].Type < list[j].Type }) + return list + } + } + // not found: append + list = append(list, c) + sort.Slice(list, func(i, j int) bool { return list[i].Type < list[j].Type }) + return list +} + + +// parseFirstCert handles both PEM and raw DER; returns the first cert if found. +func parseFirstCert(b []byte) *x509.Certificate { + // Try PEM first (possibly a chain) + var rest = b + for { + var block *pem.Block + block, rest = pem.Decode(rest) + if block == nil { + break + } + if block.Type == "CERTIFICATE" { + if c, err := x509.ParseCertificate(block.Bytes); err == nil { + return c + } + } + } + // Fallback: try raw DER bundle + if certs, err := x509.ParseCertificates(b); err == nil && len(certs) > 0 { + return certs[0] + } + return nil +} + +func firstNonEmpty(a, b string) string { + if a != "" { + return a + } + return b +} + +func safe(s string) string { + if s == "" { + return "-" + } + return s +} diff --git a/pkg/tls/templates/pretasks.tmpl.yaml b/pkg/tls/templates/pretasks.tmpl.yaml new file mode 100644 index 000000000..8235f023c --- /dev/null +++ b/pkg/tls/templates/pretasks.tmpl.yaml @@ -0,0 +1,647 @@ +--- +# ================= Canonical TLS under $SPLUNK_HOME ================= +- name: "Ensure canonical TLS dir" + file: + path: "[[ .CanonicalDir ]]" + state: directory + owner: splunk + group: splunk + mode: "0755" + +# Probe mounted inputs +- name: "Stat source files under SrcDir" + block: + - name: "Stat tls.crt" + stat: + path: "[[ .SrcDir ]]/tls.crt" + register: s_tls_crt + + - name: "Stat tls.key" + stat: + path: "[[ .SrcDir ]]/tls.key" + register: s_tls_key + + - name: "Stat ca.crt" + stat: + path: "[[ .SrcDir ]]/ca.crt" + register: s_ca_crt + + - name: "Stat chain.crt" + stat: + path: "[[ .SrcDir ]]/chain.crt" + register: s_chain_crt + + - name: "Stat tls-combined.pem (from cert-manager additionalOutputFormats)" + stat: + path: "[[ .SrcDir ]]/tls-combined.pem" + register: s_tls_combined + +# Fail early if key/cert missing +- name: "Validate required source files are present" + assert: + that: + - s_tls_crt.stat.exists + - s_tls_key.stat.exists + fail_msg: "TLS inputs not found at [[ .SrcDir ]]. Mount Secret/CSI with tls.crt and tls.key." + success_msg: "Found tls.crt and tls.key at [[ .SrcDir ]]" + +# Copy into canonical paths +- name: "Copy tls.crt -> canonical" + copy: + src: "[[ .SrcDir ]]/tls.crt" + dest: "[[ .TLSCrt ]]" + owner: splunk + group: splunk + mode: "0644" + remote_src: true + when: "s_tls_crt.stat.exists" + +- name: "Copy tls.key -> canonical (0600)" + copy: + src: "[[ .SrcDir ]]/tls.key" + dest: "[[ .TLSKey ]]" + owner: splunk + group: splunk + mode: "0600" + remote_src: true + when: "s_tls_key.stat.exists" + +- name: "Copy CA to canonical if present" + copy: + src: "[[ .SrcDir ]]/ca.crt" + dest: "[[ .CACrt ]]" + owner: splunk + group: splunk + mode: "0644" + remote_src: true + when: "s_ca_crt.stat.exists" + +- name: "Copy chain.crt as ca.crt if CA missing but chain present" + copy: + src: "[[ .SrcDir ]]/chain.crt" + dest: "[[ .CACrt ]]" + owner: splunk + group: splunk + mode: "0644" + remote_src: true + when: "(not s_ca_crt.stat.exists) and s_chain_crt.stat.exists" + +# ================= Optional trust bundle ================= +- name: "Optional trust bundle" + block: + - name: "Stat trust bundle at TrustDir/TrustKey" + stat: + path: "[[ .TrustDir ]]/[[ .TrustKey ]]" + register: s_bundle + + - name: "Copy trust bundle if present" + copy: + src: "[[ .TrustDir ]]/[[ .TrustKey ]]" + dest: "[[ .CABundle ]]" + owner: splunk + group: splunk + mode: "0644" + remote_src: true + when: "s_bundle.stat.exists" + +# ================= Build splunkd server.pem ================= +- name: Build server.pem (key + cert + optional CA) in canonical + shell: + cmd: /bin/sh -s + stdin: |- + set -e + if [ -f "[[ .TLSCrt ]]" ] && [ -f "[[ .CACrt ]]" ]; then + cat "[[ .TLSKey ]]" "[[ .TLSCrt ]]" "[[ .CACrt ]]" > "[[ .ServerPEM ]]" + else + cat "[[ .TLSKey ]]" "[[ .TLSCrt ]]" > "[[ .ServerPEM ]]" + fi + chown splunk:splunk "[[ .ServerPEM ]]" + chmod 0600 "[[ .ServerPEM ]]" + +# ================= Publish tls-combined.pem for startup precheck ================= +- name: "Ensure Splunk certs dir (for startup precheck path)" + file: + path: "[[ .SplunkHome ]]/certs" + state: directory + owner: splunk + group: splunk + mode: "0700" + +- name: "Publish tls-combined.pem from Secret (0600)" + copy: + src: "[[ .SrcDir ]]/tls-combined.pem" + dest: "[[ .SplunkHome ]]/certs/tls-combined.pem" + remote_src: true + owner: splunk + group: splunk + mode: "0600" + when: "s_tls_combined.stat.exists" + +- name: "Publish tls-combined.pem from server.pem (0600)" + copy: + src: "[[ .ServerPEM ]]" + dest: "[[ .SplunkHome ]]/certs/tls-combined.pem" + remote_src: true + owner: splunk + group: splunk + mode: "0600" + when: "not s_tls_combined.stat.exists" + +# ================= splunkd TLS (server.conf) ================= +- name: "Set [sslConfig] for splunkd (baseline)" + blockinfile: + path: "[[ .SplunkHome ]]/etc/system/local/server.conf" + marker: "# {mark} splunk-operator tls" + create: true + owner: splunk + group: splunk + mode: "0644" + block: |- + [sslConfig] + enableSplunkdSSL = true + serverCert = [[ .ServerPEM ]] + requireClientCert = false + +- name: "Set sslRootCAPath if CA exists" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/server.conf" + section: sslConfig + option: sslRootCAPath + value: "[[ .CACrt ]]" + create: true + no_extra_spaces: true + when: "s_ca_crt.stat.exists or s_chain_crt.stat.exists" + +# Optional: set [general] serverName (FQDN) if provided +[[ if ne .ServerName "" ]] +- name: "Set [general] serverName (optional)" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/server.conf" + section: general + option: serverName + value: "[[ .ServerName ]]" + create: true + no_extra_spaces: true + owner: splunk + group: splunk + mode: "0644" +[[ end ]] + +# ================= Splunk Web TLS (web.conf) ================= +- name: "Ensure web.conf [settings] startwebserver" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + section: settings + option: startwebserver + value: "1" + backup: false + create: true + no_extra_spaces: true + owner: splunk + group: splunk + mode: "0644" + +- name: "Ensure web.conf [settings] httpport" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + section: settings + option: httpport + value: "8000" + no_extra_spaces: true + +- name: "Ensure web.conf [settings] enableSplunkWebSSL" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + section: settings + option: enableSplunkWebSSL + value: "1" + no_extra_spaces: true + +- name: "Ensure web.conf [settings] privKeyPath -> tls.key" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + section: settings + option: privKeyPath + value: "[[ .TLSKey ]]" + no_extra_spaces: true + +- name: "Ensure web.conf [settings] serverCert -> tls.crt" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + section: settings + option: serverCert + value: "[[ .TLSCrt ]]" + no_extra_spaces: true + +- name: "Ensure web.conf [settings] caCertPath -> ca.crt (if CA exists)" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + section: settings + option: caCertPath + value: "[[ .CACrt ]]" + no_extra_spaces: true + when: "s_ca_crt.stat.exists or s_chain_crt.stat.exists" + +# Defensive: remove an accidental serverCert=ca.crt line if present +- name: "Remove incorrect serverCert=ca.crt assignment (defensive)" + lineinfile: + path: "[[ .SplunkHome ]]/etc/system/local/web.conf" + regexp: '^\s*serverCert\s*=\s*/opt/splunk/etc/auth/tls/ca\.crt\s*$' + state: absent + +# Optional cleanup of invalid inputs.conf http sslRootCAPath +- name: "Stat inputs.conf" + stat: + path: "[[ .SplunkHome ]]/etc/system/local/inputs.conf" + register: s_inputs + +- name: "Remove sslRootCAPath from [http] in inputs.conf if present" + ini_file: + path: "[[ .SplunkHome ]]/etc/system/local/inputs.conf" + section: http + option: sslRootCAPath + state: absent + create: false + no_extra_spaces: true + when: "s_inputs.stat.exists" + +# ================= KV-store encrypted key (optional) ================= +# Turn logs on when debugging: ansible-playbook ... -e kv_nolog=false -e kv_emit_pass=false +# In normal runs, secrets stay hidden. +[[ if .KVEnable ]] +- name: "KV: build encrypted bundle and write [kvstore] (idempotent)" + no_log: "false" + ansible.builtin.shell: + cmd: | + set -e + + SPLUNK_HOME="[[ .SplunkHome ]]" + CANON="[[ .CanonicalDir ]]" + TLSKEY="[[ .TLSKey ]]" + TLSCRT="[[ .TLSCrt ]]" + CACRT="[[ .CACrt ]]" + KVBUNDLE="[[ .KVBundlePath ]]" + PASSFILE="[[ .KVPasswordFile ]]" + + # ---- choose a working openssl (prefer Splunk's with proper env) ---- + # choose a working openssl (prefer Splunk's with proper env) + OPENSSL="" + USE_SPLUNK_OPENSSL=0 + if [ -x /opt/splunk/bin/openssl ] && \ + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl version >/dev/null 2>&1; then + OPENSSL=/opt/splunk/bin/openssl + USE_SPLUNK_OPENSSL=1 + elif command -v openssl >/dev/null 2>&1; then + OPENSSL="$(command -v openssl)" + USE_SPLUNK_OPENSSL=0 + else + echo "ERROR: no usable openssl found" >&2 + exit 1 + fi + + ossl() { + if [ "$USE_SPLUNK_OPENSSL" = "1" ]; then + # Prefix env **as separate words** before the command + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + "$OPENSSL" "$@" + else + "$OPENSSL" "$@" + fi + } + # ------------------------------------------------------------------- + + # passphrase: Secret file if provided, else random + if [ -n "$PASSFILE" ] && [ -f "$PASSFILE" ]; then + PASS="$(tr -d '\r\n' < "$PASSFILE")" + else + PASS="$(ossl rand -base64 24)" + fi + + # encrypted key for KV + ossl rsa -aes256 -passout pass:"$PASS" -in "$TLSKEY" -out "$CANON/tls_enc.key" + chown splunk:splunk "$CANON/tls_enc.key" + chmod 0600 "$CANON/tls_enc.key" + + # KV bundle (enc key + cert + optional CA) + if [ -f "$CACRT" ] && [ -s "$CACRT" ]; then + cat "$CANON/tls_enc.key" "$TLSCRT" "$CACRT" > "$KVBUNDLE" + else + cat "$CANON/tls_enc.key" "$TLSCRT" > "$KVBUNDLE" + fi + chown splunk:splunk "$KVBUNDLE" + chmod 0600 "$KVBUNDLE" + + # Idempotently upsert [kvstore] sslPassword/serverCert + SC="$SPLUNK_HOME/etc/system/local/server.conf" + TMP="$SC.tmp.$$" + mkdir -p "$(dirname "$SC")" + touch "$SC" + chown splunk:splunk "$SC" + chmod 0644 "$SC" + + awk -v pass="$PASS" -v bundle="$KVBUNDLE" ' + BEGIN{ in=0; saw1=0; saw2=0 } + function emit(){ + if(!saw1) print "sslPassword = " pass + if(!saw2) print "serverCert = " bundle + } + { + if($0 ~ /^\[kvstore\]/){ + if(in){emit()} + print; in=1; saw1=0; saw2=0; next + } + if(in){ + if($0 ~ /^\[/){ emit(); in=0 } + else { + if($0 ~ /^[ \t]*sslPassword[ \t]*=/){ print "sslPassword = " pass; saw1=1; next } + if($0 ~ /^[ \t]*serverCert[ \t]*=/){ print "serverCert = " bundle; saw2=1; next } + } + } + print + } + END{ + if(in){emit()} else { + print "[kvstore]" + print "sslPassword = " pass + print "serverCert = " bundle + } + } + ' "$SC" > "$TMP" && mv "$TMP" "$SC" + + chown splunk:splunk "$SC" + chmod 0644 "$SC" + + # Emit the pass on one line so we can capture it (optional; turn off with -e kv_emit_pass=false) + if [ "${kv_emit_pass:-true}" = "true" ]; then + printf '__KV_PASS__:%s\n' "$PASS" + fi + executable: /bin/sh + environment: + # Let the script see the toggle (so it won’t print secrets when you set kv_emit_pass=false) + kv_emit_pass: "{{ kv_emit_pass | default(true) }}" + register: kv_build +[[ end ]] + + +# ================= Overlay into /opt/splunk/auth (Option B) ================= +- name: "Ensure /opt/splunk/auth exists (real dir)" + file: + path: /opt/splunk/auth + state: directory + owner: splunk + group: splunk + mode: "0755" + +# --- stat each auth target separately (no Jinja) --- +- name: "Auth overlay: stat /opt/splunk/auth/tls.key" + stat: + path: /opt/splunk/auth/tls.key + register: s_auth_tls_key + +- name: "Auth overlay: stat /opt/splunk/auth/tls.crt" + stat: + path: /opt/splunk/auth/tls.crt + register: s_auth_tls_crt + +- name: "Auth overlay: stat /opt/splunk/auth/ca.crt" + stat: + path: /opt/splunk/auth/ca.crt + register: s_auth_ca_crt + +- name: "Auth overlay: stat /opt/splunk/auth/server.pem" + stat: + path: /opt/splunk/auth/server.pem + register: s_auth_server_pem + +# Optional extras, only relevant when KV is enabled +[[ if .KVEnable ]] +- name: "Auth overlay: stat /opt/splunk/auth/kvstore.pem" + stat: + path: /opt/splunk/auth/kvstore.pem + register: s_auth_kvstore_pem + +- name: "Auth overlay: stat /opt/splunk/auth/splunk-bundle.crt" + stat: + path: /opt/splunk/auth/splunk-bundle.crt + register: s_auth_bundle_crt + +- name: "Auth overlay: stat /opt/splunk/auth/splunk-bundle-pass.crt" + stat: + path: /opt/splunk/auth/splunk-bundle-pass.crt + register: s_auth_bundle_pass_crt + +# Extract the PASS from stdout; this keeps the value out of logs +- name: Extract KV password from kv_build output + no_log: true + set_fact: + kv_pass: >- + {{ (kv_build.stdout | default('') ) + | regex_search('__KV_PASS__:(?P

.+)$', '\g

', multiline=True) | trim }} + +# Build the splunk_ssl_password structure (your example, just Go/Jinja harmonized) +- name: Build splunk_ssl_password fact + no_log: true + set_fact: + splunk_ssl_password: + ssl: + password: "{{ kv_pass }}" + ca: "/opt/splunk/etc/auth/ca.crt" + cert: "/opt/splunk/etc/auth/splunk-bundle-pass.crt" + enable: true + conf: + - key: "server" + value: + directory: "/opt/splunk/etc/system/local" + content: + kvstore: + sslPassword: "{{ kv_pass }}" + serverCert: "/opt/splunk/etc/auth/splunk-bundle-pass.crt" + sslVerifyServerName: true + sslConfig: + sslVersions: "tls1.2" + sslVersionsForClient: "tls1.2" + requireClientCert: false + sslVerifyServerName: true + cliVerifyServerName: true + sslVerifyServerCert: true + caTrustStore: "splunk,OS" + caTrustStorePath: "/opt/splunk/etc/auth/custom-ca-bundle.crt" + node_auth: + signatureVersion: "v2" + +# Merge into splunk (recursive, append_rp for lists) +- name: Merge splunk_ssl_password into splunk + no_log: true + set_fact: + splunk: "{{ (splunk | default({})) | combine(splunk_ssl_password, recursive=true, list_merge='append_rp') }}" + +[[ end ]] + +# --- create missing links (no Jinja) --- +- name: "Auth overlay: create link tls.key" + file: + src: "[[ .TLSKey ]]" + dest: "/opt/splunk/auth/tls.key" + state: link + owner: splunk + group: splunk + when: "not s_auth_tls_key.stat.exists" + +- name: "Auth overlay: create link tls.crt" + file: + src: "[[ .TLSCrt ]]" + dest: "/opt/splunk/auth/tls.crt" + state: link + owner: splunk + group: splunk + when: "not s_auth_tls_crt.stat.exists" + +- name: "Auth overlay: create link ca.crt" + file: + src: "[[ .CACrt ]]" + dest: "/opt/splunk/auth/ca.crt" + state: link + owner: splunk + group: splunk + when: "not s_auth_ca_crt.stat.exists" + +- name: "Auth overlay: create link server.pem" + file: + src: "[[ .ServerPEM ]]" + dest: "/opt/splunk/auth/server.pem" + state: link + owner: splunk + group: splunk + when: "not s_auth_server_pem.stat.exists" + +[[ if .KVEnable ]] +- name: "Auth overlay: create link kvstore.pem" + file: + src: "[[ .KVBundlePath ]]" + dest: "/opt/splunk/auth/kvstore.pem" + state: link + owner: splunk + group: splunk + when: "not s_auth_kvstore_pem.stat.exists" + +- name: "Auth overlay: create link splunk-bundle.crt" + file: + src: "[[ .ServerPEM ]]" + dest: "/opt/splunk/auth/splunk-bundle.crt" + state: link + owner: splunk + group: splunk + when: "not s_auth_bundle_crt.stat.exists" + +- name: "Auth overlay: create link splunk-bundle-pass.crt" + file: + src: "[[ .KVBundlePath ]]" + dest: "/opt/splunk/auth/splunk-bundle-pass.crt" + state: link + owner: splunk + group: splunk + when: "not s_auth_bundle_pass_crt.stat.exists" +[[ end ]] + +# --- retarget wrong symlinks (no Jinja) --- +- name: "Auth overlay: retarget tls.key if wrong" + file: + src: "[[ .TLSKey ]]" + dest: "/opt/splunk/auth/tls.key" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_tls_key.stat.islnk | default(false) and (s_auth_tls_key.stat.lnk_source | default('')) != '[[ .TLSKey ]]'" + +- name: "Auth overlay: retarget tls.crt if wrong" + file: + src: "[[ .TLSCrt ]]" + dest: "/opt/splunk/auth/tls.crt" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_tls_crt.stat.islnk | default(false) and (s_auth_tls_crt.stat.lnk_source | default('')) != '[[ .TLSCrt ]]'" + +- name: "Auth overlay: retarget ca.crt if wrong" + file: + src: "[[ .CACrt ]]" + dest: "/opt/splunk/auth/ca.crt" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_ca_crt.stat.islnk | default(false) and (s_auth_ca_crt.stat.lnk_source | default('')) != '[[ .CACrt ]]'" + +- name: "Auth overlay: retarget server.pem if wrong" + file: + src: "[[ .ServerPEM ]]" + dest: "/opt/splunk/auth/server.pem" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_server_pem.stat.islnk | default(false) and (s_auth_server_pem.stat.lnk_source | default('')) != '[[ .ServerPEM ]]'" + +[[ if .KVEnable ]] +- name: "Auth overlay: retarget kvstore.pem if wrong" + file: + src: "[[ .KVBundlePath ]]" + dest: "/opt/splunk/auth/kvstore.pem" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_kvstore_pem.stat.islnk | default(false) and (s_auth_kvstore_pem.stat.lnk_source | default('')) != '[[ .KVBundlePath ]]'" + +- name: "Auth overlay: retarget splunk-bundle.crt if wrong" + file: + src: "[[ .ServerPEM ]]" + dest: "/opt/splunk/auth/splunk-bundle.crt" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_bundle_crt.stat.islnk | default(false) and (s_auth_bundle_crt.stat.lnk_source | default('')) != '[[ .ServerPEM ]]'" + +- name: "Auth overlay: retarget splunk-bundle-pass.crt if wrong" + file: + src: "[[ .KVBundlePath ]]" + dest: "/opt/splunk/auth/splunk-bundle-pass.crt" + state: link + owner: splunk + group: splunk + force: yes + when: "s_auth_bundle_pass_crt.stat.islnk | default(false) and (s_auth_bundle_pass_crt.stat.lnk_source | default('')) != '[[ .KVBundlePath ]]'" +[[ end ]] + +# ================= Final safety: assert canonical perms ================= +- name: "Re-assert canonical file perms" + file: + path: "[[ .TLSKey ]]" + owner: splunk + group: splunk + mode: "0600" + +- name: "Re-assert canonical file perms (tls.crt)" + file: + path: "[[ .TLSCrt ]]" + owner: splunk + group: splunk + mode: "0644" + +- name: "Re-assert canonical file perms (server.pem)" + file: + path: "[[ .ServerPEM ]]" + owner: splunk + group: splunk + mode: "0600" + +- name: "Re-assert canonical file perms (ca.crt)" + file: + path: "[[ .CACrt ]]" + owner: splunk + group: splunk + mode: "0644" diff --git a/pkg/tls/validate.go b/pkg/tls/validate.go new file mode 100644 index 000000000..355e8d34d --- /dev/null +++ b/pkg/tls/validate.go @@ -0,0 +1,21 @@ +// pkg/splunk/enterprise/tls/validate.go + +package tls + +import ( + "fmt" + v4 "github.com/splunk/splunk-operator/api/v4" +) + +func ValidateTLSSpec(t *v4.TLSConfig) error { + if t == nil { + return nil + } + if t.KVEncryptedKey != nil && t.KVEncryptedKey.Enabled { + // nothing hard to validate; optional SecretKeySelector is fine + if t.CanonicalDir == "" { + return fmt.Errorf("tls.canonicalDir is required when kvEncryptedKey.enabled=true") + } + } + return nil +} From 3549e0cca354bd8e84d908d4cdfbec6e66733346 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Sat, 4 Oct 2025 20:43:38 -0700 Subject: [PATCH 2/5] adding CRD changes to support cert-manager --- ...enterprise.splunk.com_clustermanagers.yaml | 108 +++++++ .../enterprise.splunk.com_clustermasters.yaml | 108 +++++++ ...enterprise.splunk.com_indexerclusters.yaml | 216 ++++++++++++++ ...enterprise.splunk.com_licensemanagers.yaml | 108 +++++++ .../enterprise.splunk.com_licensemasters.yaml | 108 +++++++ ...erprise.splunk.com_monitoringconsoles.yaml | 216 ++++++++++++++ ...erprise.splunk.com_searchheadclusters.yaml | 216 ++++++++++++++ .../enterprise.splunk.com_standalones.yaml | 275 ++++++++++++++++++ 8 files changed, 1355 insertions(+) diff --git a/config/crd/bases/enterprise.splunk.com_clustermanagers.yaml b/config/crd/bases/enterprise.splunk.com_clustermanagers.yaml index a899c91d4..d005a3b8f 100644 --- a/config/crd/bases/enterprise.splunk.com_clustermanagers.yaml +++ b/config/crd/bases/enterprise.splunk.com_clustermanagers.yaml @@ -1390,6 +1390,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2338,6 +2339,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2571,6 +2678,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_clustermasters.yaml b/config/crd/bases/enterprise.splunk.com_clustermasters.yaml index 202cd5e72..73dead881 100644 --- a/config/crd/bases/enterprise.splunk.com_clustermasters.yaml +++ b/config/crd/bases/enterprise.splunk.com_clustermasters.yaml @@ -1386,6 +1386,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2334,6 +2335,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2567,6 +2674,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_indexerclusters.yaml b/config/crd/bases/enterprise.splunk.com_indexerclusters.yaml index a068f17c9..974b9356e 100644 --- a/config/crd/bases/enterprise.splunk.com_indexerclusters.yaml +++ b/config/crd/bases/enterprise.splunk.com_indexerclusters.yaml @@ -1238,6 +1238,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2074,6 +2075,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2307,6 +2414,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: @@ -5410,6 +5518,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -6246,6 +6355,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -6479,6 +6694,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_licensemanagers.yaml b/config/crd/bases/enterprise.splunk.com_licensemanagers.yaml index 2df56e71c..6ff2df10c 100644 --- a/config/crd/bases/enterprise.splunk.com_licensemanagers.yaml +++ b/config/crd/bases/enterprise.splunk.com_licensemanagers.yaml @@ -1380,6 +1380,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2211,6 +2212,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2444,6 +2551,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_licensemasters.yaml b/config/crd/bases/enterprise.splunk.com_licensemasters.yaml index 0ccb1d29f..c37d4d478 100644 --- a/config/crd/bases/enterprise.splunk.com_licensemasters.yaml +++ b/config/crd/bases/enterprise.splunk.com_licensemasters.yaml @@ -1375,6 +1375,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2206,6 +2207,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2439,6 +2546,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_monitoringconsoles.yaml b/config/crd/bases/enterprise.splunk.com_monitoringconsoles.yaml index bb6302ff8..73e50a78a 100644 --- a/config/crd/bases/enterprise.splunk.com_monitoringconsoles.yaml +++ b/config/crd/bases/enterprise.splunk.com_monitoringconsoles.yaml @@ -1382,6 +1382,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2213,6 +2214,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2446,6 +2553,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: @@ -5899,6 +6007,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -6730,6 +6839,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -6963,6 +7178,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_searchheadclusters.yaml b/config/crd/bases/enterprise.splunk.com_searchheadclusters.yaml index adfde431a..06e0aee71 100644 --- a/config/crd/bases/enterprise.splunk.com_searchheadclusters.yaml +++ b/config/crd/bases/enterprise.splunk.com_searchheadclusters.yaml @@ -1388,6 +1388,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2224,6 +2225,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2457,6 +2564,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: @@ -6249,6 +6357,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -7085,6 +7194,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -7318,6 +7533,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: diff --git a/config/crd/bases/enterprise.splunk.com_standalones.yaml b/config/crd/bases/enterprise.splunk.com_standalones.yaml index 2964128a8..e0938361f 100644 --- a/config/crd/bases/enterprise.splunk.com_standalones.yaml +++ b/config/crd/bases/enterprise.splunk.com_standalones.yaml @@ -1383,6 +1383,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -2335,6 +2336,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -2568,6 +2675,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: @@ -6144,6 +6252,7 @@ spec: environment variables) type: string imagePullPolicy: + default: IfNotPresent description: 'Sets pull policy for all images (either “Always” or the default: “IfNotPresent”)' enum: @@ -7096,6 +7205,112 @@ spec: format: int32 type: integer type: object + tls: + description: |- + TLSConfig controls how certs/keys/CA are presented to Splunk. + All content is copied/symlinked into CanonicalDir under $SPLUNK_HOME. + properties: + canonicalDir: + description: Canonical destination inside $SPLUNK_HOME, defaults + to /opt/splunk/etc/auth/tls + type: string + csi: + properties: + dnsNames: + description: DNS SANs for the leaf + items: + type: string + type: array + duration: + description: Durations parsed by cert-manager (example "2160h") + type: string + issuerRefKind: + description: cert-manager CSI driver attributes + enum: + - Issuer + - ClusterIssuer + type: string + issuerRefName: + minLength: 1 + type: string + keyAlgorithm: + enum: + - rsa + - ecdsa + type: string + keySize: + type: integer + renewBefore: + type: string + type: object + kvEncryptedKey: + description: |- + KVEncryptedKey enables building a separate PEM for KV store + that contains an AES-256 encrypted private key, and writes + [kvstore] sslPassword + serverCert in server.conf accordingly. + Defaults to disabled for simplicity and reliability. + properties: + bundleFile: + description: |- + BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + Default: "kvstore.pem" + type: string + enabled: + description: 'Enabled toggles the feature on or off. Default: + false.' + type: boolean + passwordSecretRef: + description: |- + PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + to encrypt the key. If omitted, we will auto-generate a random + base64 passphrase at runtime inside the pod. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + provider: + enum: + - Secret + - CSI + type: string + secretRef: + properties: + name: + description: 'Secret with keys: tls.crt, tls.key, ca.crt (optional)' + minLength: 1 + type: string + type: object + trustBundle: + description: Optional Trust Bundle mounted as a Secret produced + by trust-manager + properties: + key: + type: string + secretName: + description: Optional trust-manager bundle Secret containing + the CA bundle + type: string + type: object + type: object tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -7329,6 +7544,7 @@ spec: type: string type: object volumes: + default: [] description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ items: @@ -9527,6 +9743,65 @@ spec: telAppInstalled: description: Telemetry App installation flag type: boolean + tls: + properties: + canonicalDir: + type: string + conditions: + items: + properties: + lastTransitionTime: + format: date-time + type: string + message: + type: string + reason: + type: string + status: + type: string + type: + type: string + type: object + type: array + lastObserved: + format: date-time + type: string + observed: + properties: + hash: + description: |- + Hash over cert material we observe + - for Secret: sha256 over tls.crt, tls.key, ca.crt if present + - for CSI: sha256 over CSI attributes (best-effort) + type: string + leafSHA256: + description: Leaf certificate facts if we can parse tls.crt + type: string + notAfter: + format: date-time + type: string + notBefore: + format: date-time + type: string + secretResourceVersion: + description: Secret resourceVersion if provider=Secret + type: string + serialNumber: + type: string + source: + description: Where we sourced material from - e.g. "Secret/" + or "CSI:Issuer/Name" + type: string + type: object + podTemplateChecksum: + type: string + preTasksHash: + type: string + provider: + type: string + trustBundleHash: + type: string + type: object type: object type: object served: true From 7498d2e6286a56a4321e1f78cfcdef37c41835b5 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Sun, 5 Oct 2025 09:43:29 -0700 Subject: [PATCH 3/5] updated to work default setup for tmpl file --- Dockerfile | 2 +- api/v4/tls_types.go | 28 +- api/v4/zz_generated.deepcopy.go | 25 + go.mod | 4 +- pkg/splunk/enterprise/standalone.go | 2 +- pkg/splunk/enterprise/tls_configuraiton.go | 2 +- pkg/terms/terms.go | 6 +- pkg/tls/constants.go | 26 +- pkg/tls/defaults.go | 7 + pkg/tls/mutate.go | 38 +- pkg/tls/pretasks_renderer_template_test.go | 2 +- pkg/tls/pretasks_renderer_test.go | 8 +- pkg/tls/status.go | 1 - pkg/tls/templates/pretasks.tmpl.yaml | 673 +++++---------------- pkg/tls/validate.go | 24 +- 15 files changed, 264 insertions(+), 584 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1da1087f2..096531051 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,7 +21,7 @@ RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ -COPY internal/controller/ internal/controller/ +COPY internal/ internal/ COPY pkg/ pkg/ COPY tools/ tools/ COPY hack hack/ diff --git a/api/v4/tls_types.go b/api/v4/tls_types.go index 0a1c691f5..89b6fafda 100644 --- a/api/v4/tls_types.go +++ b/api/v4/tls_types.go @@ -82,7 +82,7 @@ type TLSConfig struct { SecretRef *TLSSecretRef `json:"secretRef,omitempty"` // +optional CSI *TLSCSI `json:"csi,omitempty"` - // Canonical destination inside $SPLUNK_HOME, defaults to /opt/splunk/etc/auth/tls + // Canonical destination inside $SPLUNK_HOME, defaults to /opt/splunk/etc/auth // +optional CanonicalDir string `json:"canonicalDir,omitempty"` // Optional Trust Bundle mounted as a Secret produced by trust-manager @@ -90,24 +90,24 @@ type TLSConfig struct { TrustBundle *TrustBundle `json:"trustBundle,omitempty"` // KVEncryptedKey enables building a separate PEM for KV store - // that contains an AES-256 encrypted private key, and writes - // [kvstore] sslPassword + serverCert in server.conf accordingly. - // Defaults to disabled for simplicity and reliability. - KVEncryptedKey *KVEncryptedKeySpec `json:"kvEncryptedKey,omitempty"` + // that contains an AES-256 encrypted private key, and writes + // [kvstore] sslPassword + serverCert in server.conf accordingly. + // Defaults to disabled for simplicity and reliability. + KVEncryptedKey *KVEncryptedKeySpec `json:"kvEncryptedKey,omitempty"` } type KVEncryptedKeySpec struct { - // Enabled toggles the feature on or off. Default: false. - Enabled bool `json:"enabled"` + // Enabled toggles the feature on or off. Default: false. + Enabled bool `json:"enabled"` - // PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) - // to encrypt the key. If omitted, we will auto-generate a random - // base64 passphrase at runtime inside the pod. - PasswordSecretRef *corev1.SecretKeySelector `json:"passwordSecretRef,omitempty"` + // PasswordSecretRef, if set, provides the passphrase (utf-8, no newline) + // to encrypt the key. If omitted, we will auto-generate a random + // base64 passphrase at runtime inside the pod. + PasswordSecretRef *corev1.SecretKeySelector `json:"passwordSecretRef,omitempty"` - // BundleFile is the filename (under canonicalDir) for the KV bundle PEM. - // Default: "kvstore.pem" - BundleFile string `json:"bundleFile,omitempty"` + // BundleFile is the filename (under canonicalDir) for the KV bundle PEM. + // Default: "kvstore.pem" + BundleFile string `json:"bundleFile,omitempty"` } type TLSCondition struct { diff --git a/api/v4/zz_generated.deepcopy.go b/api/v4/zz_generated.deepcopy.go index 34f47efa4..fcca6e249 100644 --- a/api/v4/zz_generated.deepcopy.go +++ b/api/v4/zz_generated.deepcopy.go @@ -560,6 +560,26 @@ func (in *IndexerClusterStatus) DeepCopy() *IndexerClusterStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KVEncryptedKeySpec) DeepCopyInto(out *KVEncryptedKeySpec) { + *out = *in + if in.PasswordSecretRef != nil { + in, out := &in.PasswordSecretRef, &out.PasswordSecretRef + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KVEncryptedKeySpec. +func (in *KVEncryptedKeySpec) DeepCopy() *KVEncryptedKeySpec { + if in == nil { + return nil + } + out := new(KVEncryptedKeySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LicenseManager) DeepCopyInto(out *LicenseManager) { *out = *in @@ -1167,6 +1187,11 @@ func (in *TLSConfig) DeepCopyInto(out *TLSConfig) { *out = new(TrustBundle) **out = **in } + if in.KVEncryptedKey != nil { + in, out := &in.KVEncryptedKey, &out.KVEncryptedKey + *out = new(KVEncryptedKeySpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSConfig. diff --git a/go.mod b/go.mod index 9a87e5cf7..f3ab21dff 100644 --- a/go.mod +++ b/go.mod @@ -25,13 +25,13 @@ require ( github.com/wk8/go-ordered-map/v2 v2.1.7 go.uber.org/zap v1.26.0 google.golang.org/api v0.126.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.31.0 k8s.io/apiextensions-apiserver v0.31.0 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 k8s.io/kubectl v0.26.2 sigs.k8s.io/controller-runtime v0.19.0 - sigs.k8s.io/yaml v1.4.0 ) require ( @@ -152,7 +152,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiserver v0.31.0 // indirect k8s.io/component-base v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect @@ -161,4 +160,5 @@ require ( sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/pkg/splunk/enterprise/standalone.go b/pkg/splunk/enterprise/standalone.go index 85beb3915..0120d2e90 100644 --- a/pkg/splunk/enterprise/standalone.go +++ b/pkg/splunk/enterprise/standalone.go @@ -24,9 +24,9 @@ import ( enterpriseApi "github.com/splunk/splunk-operator/api/v4" splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" - "github.com/splunk/splunk-operator/pkg/tls" splctrl "github.com/splunk/splunk-operator/pkg/splunk/splkcontroller" splutil "github.com/splunk/splunk-operator/pkg/splunk/util" + "github.com/splunk/splunk-operator/pkg/tls" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" diff --git a/pkg/splunk/enterprise/tls_configuraiton.go b/pkg/splunk/enterprise/tls_configuraiton.go index 7adbee389..a4cd0d868 100644 --- a/pkg/splunk/enterprise/tls_configuraiton.go +++ b/pkg/splunk/enterprise/tls_configuraiton.go @@ -15,8 +15,8 @@ import ( v4 "github.com/splunk/splunk-operator/api/v4" splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" - tls "github.com/splunk/splunk-operator/pkg/tls" "github.com/splunk/splunk-operator/pkg/splunk/splkcontroller" + tls "github.com/splunk/splunk-operator/pkg/tls" ) func mutateTLS(ctx context.Context, c splcommon.ControllerClient, ss *appsv1.StatefulSet, cr *v4.Standalone) error { diff --git a/pkg/terms/terms.go b/pkg/terms/terms.go index d22e7f6c3..4dab1f318 100644 --- a/pkg/terms/terms.go +++ b/pkg/terms/terms.go @@ -7,8 +7,8 @@ import ( ) const ( - EnvVarName = "SPLUNK_GENERAL_TERMS" - ExpectedFlag = "--accept-sgt-current-at-splunk-com" + EnvVarName = "SPLUNK_GENERAL_TERMS" + ExpectedFlag = "--accept-sgt-current-at-splunk-com" ) var accepted atomic.Bool @@ -20,4 +20,4 @@ func InitFromEnv() { func Accepted() bool { return accepted.Load() -} \ No newline at end of file +} diff --git a/pkg/tls/constants.go b/pkg/tls/constants.go index 3853a895e..20f971298 100644 --- a/pkg/tls/constants.go +++ b/pkg/tls/constants.go @@ -2,19 +2,19 @@ package tls // Canonical locations inside the Splunk container const ( - SplunkHomeDefault = "/opt/splunk" - CanonicalTLSDir = "/opt/splunk/etc/auth" // default; overridable by spec.tls.canonicalDir + SplunkHomeDefault = "/opt/splunk" + CanonicalTLSDir = "/opt/splunk/etc/auth" // default; overridable by spec.tls.canonicalDir //CanonicalTLSDir = "/opt/splunk/etc/auth" // default; overridable by spec.tls.canonicalDir // Source mounts - TLSSrcMountDir = "/mnt/certs" // Secret or CSI - TrustSrcMountDir = "/mnt/trust" // optional trust bundle Secret - KVPassSrcMountDir = "/mnt/kvpass"// optional secret for kv password + TLSSrcMountDir = "/mnt/certs" // Secret or CSI + TrustSrcMountDir = "/mnt/trust" // optional trust bundle Secret + KVPassSrcMountDir = "/mnt/kvpass" // optional secret for kv password // PRE-TASKS mount - PreTasksMountDir = "/mnt/pre" - PreTasksFilename = "tls.yml" // combined Go-templated playbook - PreTasksFileURI = "file:///mnt/pre/tls.yml" // SPLUNK_ANSIBLE_PRE_TASKS value + PreTasksMountDir = "/mnt/pre" + PreTasksFilename = "tls.yml" // combined Go-templated playbook + PreTasksFileURI = "file:///mnt/pre/tls.yml" // SPLUNK_ANSIBLE_PRE_TASKS value // Canonical filenames under CanonicalTLSDir TLSCrtName = "tls.crt" @@ -25,15 +25,15 @@ const ( KVBundleDefaultName = "kvstore.pem" // if kvEncryptedKey.enabled // K8s resource names (volume/configmap keys) - PreTasksCMKey = "tls.yml" // key in ConfigMap data + PreTasksCMKey = "tls.yml" // key in ConfigMap data // Pod template annotations (visible diff even with OnDelete) - AnnTLSChecksum = "enterprise.splunk.com/tls-checksum" // sha256 of observed TLS inputs - AnnPreTasksChecksum = "enterprise.splunk.com/pretasks-checksum"// sha256 of rendered pretasks + AnnTLSChecksum = "enterprise.splunk.com/tls-checksum" // sha256 of observed TLS inputs + AnnPreTasksChecksum = "enterprise.splunk.com/pretasks-checksum" // sha256 of rendered pretasks // Env - EnvPreTasks = "SPLUNK_ANSIBLE_PRE_TASKS" - EnvStartArgs = "SPLUNK_START_ARGS" + EnvPreTasks = "SPLUNK_ANSIBLE_PRE_TASKS" + EnvStartArgs = "SPLUNK_START_ARGS" ) // Small helper for int32 pointers diff --git a/pkg/tls/defaults.go b/pkg/tls/defaults.go index 0178dab91..0103ddffa 100644 --- a/pkg/tls/defaults.go +++ b/pkg/tls/defaults.go @@ -33,6 +33,9 @@ type Data struct { // Optional serverName for [general]; the caller sets if desired ServerName string + + KVBundleAliasCRT string // CanonicalDir + "/splunk-bundle-pass.crt" + KVBundleAliasPEM string // CanonicalDir + "/splunk-bundle-pass.pem" } // defaultsFor applies operator defaults using constants from constants.go. @@ -77,6 +80,10 @@ func defaultsFor(spec *v4.TLSConfig) Data { // d.KVPasswordFile is set by the mutator if you mount a password Secret at KVPassSrcMountDir. } + // Always expose the two common alias names in the canonical dir + d.KVBundleAliasCRT = d.CanonicalDir + "/splunk-bundle-pass.crt" + d.KVBundleAliasPEM = d.CanonicalDir + "/splunk-bundle-pass.pem" + // d.ServerName (and d.KVPasswordFile) are set by the caller (renderer/mutator) as needed. return d } diff --git a/pkg/tls/mutate.go b/pkg/tls/mutate.go index dbda298cb..2f9bf9b6f 100644 --- a/pkg/tls/mutate.go +++ b/pkg/tls/mutate.go @@ -13,16 +13,16 @@ import ( // InjectTLSForPodTemplate wires TLS mounts/env/annotations into the PodTemplate. // // It expects that the reconciler already: -// * created/updated a ConfigMap named preTasksCMName with key splunk.PreTasksCMKey holding the rendered pretasks YAML -// * computed observedTLSChecksum (e.g., sha256 of Secret data or CSI attributes) -// * computed preTasksHash (sha256 of rendered pretasks content) +// - created/updated a ConfigMap named preTasksCMName with key splunk.PreTasksCMKey holding the rendered pretasks YAML +// - computed observedTLSChecksum (e.g., sha256 of Secret data or CSI attributes) +// - computed preTasksHash (sha256 of rendered pretasks content) // // This function: -// * mounts the pretasks CM at /mnt/pre/tls.yml and sets SPLUNK_ANSIBLE_PRE_TASKS -// * mounts Secret or CSI at /mnt/certs -// * mounts optional trust-bundle Secret at /mnt/trust -// * mounts optional KV password Secret at /mnt/kvpass (and returns its file path for renderer convenience) -// * annotates the pod template with TLS and pretask checksums (visible diff with OnDelete strategy) +// - mounts the pretasks CM at /mnt/pre/tls.yml and sets SPLUNK_ANSIBLE_PRE_TASKS +// - mounts Secret or CSI at /mnt/certs +// - mounts optional trust-bundle Secret at /mnt/trust +// - mounts optional KV password Secret at /mnt/kvpass (and returns its file path for renderer convenience) +// - annotates the pod template with TLS and pretask checksums (visible diff with OnDelete strategy) func InjectTLSForPodTemplate( podTemplate *corev1.PodTemplateSpec, spec *v4.TLSConfig, @@ -85,9 +85,9 @@ func InjectTLSForPodTemplate( } attrs := map[string]string{ "csi.storage.k8s.io/ephemeral": "true", - "issuer-name": spec.CSI.IssuerRefName, - "issuer-kind": spec.CSI.IssuerRefKind, - "issuer-group": "cert-manager.io", + "issuer-name": spec.CSI.IssuerRefName, + "issuer-kind": spec.CSI.IssuerRefKind, + "issuer-group": "cert-manager.io", } if len(spec.CSI.DNSNames) > 0 { attrs["dns-names"] = joinCSV(spec.CSI.DNSNames) @@ -168,17 +168,25 @@ func InjectTLSForPodTemplate( return kvPassFile, fmt.Errorf("no containers in pod template") } spl := &podTemplate.Spec.Containers[0] - add := func(n, v string) { + add := func(n, v string) { found := false - for i := range spl.Env { if spl.Env[i].Name == n { spl.Env[i].Value = v; found = true; break } } - if !found { spl.Env = append(spl.Env, corev1.EnvVar{Name: n, Value: v}) } + for i := range spl.Env { + if spl.Env[i].Name == n { + spl.Env[i].Value = v + found = true + break + } + } + if !found { + spl.Env = append(spl.Env, corev1.EnvVar{Name: n, Value: v}) + } } // Web SSL (keeps Ansible roles consistent with our file layout) add("SPLUNK_HTTP_ENABLESSL", "1") add("SPLUNK_HTTP_ENABLESSL_CERT", fmt.Sprintf("%s/%s", CanonicalTLSDir, TLSCrtName)) add("SPLUNK_HTTP_ENABLESSL_PRIVKEY", fmt.Sprintf("%s/%s", CanonicalTLSDir, TLSKeyName)) - add("SPLUNK_HTTP_ENABLESSL_PRIVKEY_PASSWORD", "") + //add("SPLUNK_HTTP_ENABLESSL_PRIVKEY_PASSWORD", "") // splunkd SSL (we point to our combined server.pem and CA) add("SPLUNKD_SSL_ENABLE", "true") diff --git a/pkg/tls/pretasks_renderer_template_test.go b/pkg/tls/pretasks_renderer_template_test.go index 0cef58a24..1dd3642c9 100644 --- a/pkg/tls/pretasks_renderer_template_test.go +++ b/pkg/tls/pretasks_renderer_template_test.go @@ -16,7 +16,7 @@ import ( func render(t *testing.T, kv bool, serverName string) string { t.Helper() spec := &v4.TLSConfig{ - CanonicalDir: "/opt/splunk/etc/auth/tls", + CanonicalDir: "/opt/splunk/etc/auth", } if kv { spec.KVEncryptedKey = &v4.KVEncryptedKeySpec{Enabled: true} diff --git a/pkg/tls/pretasks_renderer_test.go b/pkg/tls/pretasks_renderer_test.go index ac6ea0c72..8b127baf2 100644 --- a/pkg/tls/pretasks_renderer_test.go +++ b/pkg/tls/pretasks_renderer_test.go @@ -71,7 +71,7 @@ func TestRender_Success_WithAndWithoutKV(t *testing.T) { withTempTemplate(t, goodTemplate, func() { // Case 1: KV disabled, no serverName spec := &v4.TLSConfig{ - CanonicalDir: "/opt/splunk/etc/auth/tls", + CanonicalDir: "/opt/splunk/etc/auth", // KVEncryptedKey is nil, so KVEnable=false } yml, sum, err := Render(spec, "", "") @@ -93,7 +93,7 @@ func TestRender_Success_WithAndWithoutKV(t *testing.T) { // Case 2: KV enabled, with serverName spec2 := &v4.TLSConfig{ - CanonicalDir: "/opt/splunk/etc/auth/tls", + CanonicalDir: "/opt/splunk/etc/auth", KVEncryptedKey: &v4.KVEncryptedKeySpec{ Enabled: true, BundleFile: "", // default path from renderer @@ -117,7 +117,7 @@ func TestRender_Success_WithAndWithoutKV(t *testing.T) { func TestRender_Failure_ParseError(t *testing.T) { withTempTemplate(t, badTemplate, func() { - spec := &v4.TLSConfig{CanonicalDir: "/opt/splunk/etc/auth/tls"} + spec := &v4.TLSConfig{CanonicalDir: "/opt/splunk/etc/auth"} _, _, err := Render(spec, "", "") if err == nil { t.Fatal("expected parse error, got nil") @@ -130,7 +130,7 @@ func TestRender_Failure_ParseError(t *testing.T) { func TestRender_Failure_MissingKey(t *testing.T) { withTempTemplate(t, missingKeyTpl, func() { - spec := &v4.TLSConfig{CanonicalDir: "/opt/splunk/etc/auth/tls"} + spec := &v4.TLSConfig{CanonicalDir: "/opt/splunk/etc/auth"} _, _, err := Render(spec, "", "") if err == nil { t.Fatal("expected missingkey error, got nil") diff --git a/pkg/tls/status.go b/pkg/tls/status.go index 54e50ca7e..eab5045a6 100644 --- a/pkg/tls/status.go +++ b/pkg/tls/status.go @@ -272,7 +272,6 @@ func upsert(list []v4.TLSCondition, c v4.TLSCondition) []v4.TLSCondition { return list } - // parseFirstCert handles both PEM and raw DER; returns the first cert if found. func parseFirstCert(b []byte) *x509.Certificate { // Try PEM first (possibly a chain) diff --git a/pkg/tls/templates/pretasks.tmpl.yaml b/pkg/tls/templates/pretasks.tmpl.yaml index 8235f023c..9ad9b6500 100644 --- a/pkg/tls/templates/pretasks.tmpl.yaml +++ b/pkg/tls/templates/pretasks.tmpl.yaml @@ -26,16 +26,6 @@ path: "[[ .SrcDir ]]/ca.crt" register: s_ca_crt - - name: "Stat chain.crt" - stat: - path: "[[ .SrcDir ]]/chain.crt" - register: s_chain_crt - - - name: "Stat tls-combined.pem (from cert-manager additionalOutputFormats)" - stat: - path: "[[ .SrcDir ]]/tls-combined.pem" - register: s_tls_combined - # Fail early if key/cert missing - name: "Validate required source files are present" assert: @@ -45,8 +35,8 @@ fail_msg: "TLS inputs not found at [[ .SrcDir ]]. Mount Secret/CSI with tls.crt and tls.key." success_msg: "Found tls.crt and tls.key at [[ .SrcDir ]]" -# Copy into canonical paths -- name: "Copy tls.crt -> canonical" +# Copy into canonical paths (explicit target filenames) +- name: "Copy tls.crt -> [[ .TLSCrt ]]" copy: src: "[[ .SrcDir ]]/tls.crt" dest: "[[ .TLSCrt ]]" @@ -54,9 +44,9 @@ group: splunk mode: "0644" remote_src: true - when: "s_tls_crt.stat.exists" + when: s_tls_crt.stat.exists -- name: "Copy tls.key -> canonical (0600)" +- name: "Copy tls.key -> [[ .TLSKey ]] (0600)" copy: src: "[[ .SrcDir ]]/tls.key" dest: "[[ .TLSKey ]]" @@ -64,9 +54,9 @@ group: splunk mode: "0600" remote_src: true - when: "s_tls_key.stat.exists" + when: s_tls_key.stat.exists -- name: "Copy CA to canonical if present" +- name: "Copy ca.crt -> [[ .CACrt ]] (if present)" copy: src: "[[ .SrcDir ]]/ca.crt" dest: "[[ .CACrt ]]" @@ -74,27 +64,17 @@ group: splunk mode: "0644" remote_src: true - when: "s_ca_crt.stat.exists" - -- name: "Copy chain.crt as ca.crt if CA missing but chain present" - copy: - src: "[[ .SrcDir ]]/chain.crt" - dest: "[[ .CACrt ]]" - owner: splunk - group: splunk - mode: "0644" - remote_src: true - when: "(not s_ca_crt.stat.exists) and s_chain_crt.stat.exists" + when: s_ca_crt.stat.exists -# ================= Optional trust bundle ================= -- name: "Optional trust bundle" +# ================= Optional trust bundle (from separate Secret) ================= +- name: "Optional trust bundle from [[ .TrustDir ]]/[[ .TrustKey ]] -> [[ .CABundle ]]" block: - - name: "Stat trust bundle at TrustDir/TrustKey" + - name: "Stat trust bundle source" stat: path: "[[ .TrustDir ]]/[[ .TrustKey ]]" register: s_bundle - - name: "Copy trust bundle if present" + - name: "Copy provided trust bundle" copy: src: "[[ .TrustDir ]]/[[ .TrustKey ]]" dest: "[[ .CABundle ]]" @@ -102,362 +82,171 @@ group: splunk mode: "0644" remote_src: true - when: "s_bundle.stat.exists" - -# ================= Build splunkd server.pem ================= -- name: Build server.pem (key + cert + optional CA) in canonical - shell: - cmd: /bin/sh -s - stdin: |- - set -e - if [ -f "[[ .TLSCrt ]]" ] && [ -f "[[ .CACrt ]]" ]; then - cat "[[ .TLSKey ]]" "[[ .TLSCrt ]]" "[[ .CACrt ]]" > "[[ .ServerPEM ]]" + when: s_bundle.stat.exists + +# ================= Build custom trust store (customer behavior) ================= +- name: "Detect OS CA bundle path" + shell: | + for f in \ + /etc/pki/tls/certs/ca-bundle.crt \ + /etc/ssl/certs/ca-certificates.crt \ + /etc/ssl/ca-bundle.pem \ + /etc/ssl/cert.pem + do + [ -r "$f" ] && { echo "$f"; exit 0; } + done + exit 1 + register: _os_ca + changed_when: false + failed_when: false + +- name: "Build trust bundle at [[ .CABundle ]] (CA + OS bundle if present)" + shell: | + set -e + dst="[[ .CABundle ]]" + ca="[[ .CACrt ]]" + os="{{ _os_ca.stdout | default('') }}" + tmp="$(mktemp)" + if [ -s "$ca" ] && [ -n "$os" ] && [ -s "$os" ]; then + cat "$ca" "$os" > "$tmp" + elif [ -s "$ca" ]; then + cat "$ca" > "$tmp" + elif [ -n "$os" ] && [ -s "$os" ]; then + cat "$os" > "$tmp" + else + # nothing to build + exit 0 + fi + install -o splunk -g splunk -m 0644 -T "$tmp" "$dst" + rm -f "$tmp" + when: not (s_bundle.stat.exists | default(false)) + +# ================= Split tls.crt into S-* (leaf + chain) ================= +- name: "Split [[ .TLSCrt ]] into S-* (leaf + chain)" + shell: | + set -e + cd "[[ .CanonicalDir ]]" + rm -f S-* || true + awk 'BEGIN{n=-1} + /-----BEGIN CERTIFICATE-----/{n++; file=sprintf("S-%02d",n)} + {if(n>=0) print > file} + /-----END CERTIFICATE-----/{close(file)}' "[[ .TLSCrt ]]" + test -s S-00 + changed_when: true + +# ================= Generate password (prefer Splunk OpenSSL; fallback safe) ================= +- name: "Generate kv/tls key password" + no_log: true + shell: | + set -e + if LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" /opt/splunk/bin/openssl version >/dev/null 2>&1; then + if LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" /opt/splunk/bin/openssl rand -base64 24 >/dev/null 2>&1; then + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" /opt/splunk/bin/openssl rand -base64 24 else - cat "[[ .TLSKey ]]" "[[ .TLSCrt ]]" > "[[ .ServerPEM ]]" + dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64 fi - chown splunk:splunk "[[ .ServerPEM ]]" - chmod 0600 "[[ .ServerPEM ]]" - -# ================= Publish tls-combined.pem for startup precheck ================= -- name: "Ensure Splunk certs dir (for startup precheck path)" - file: - path: "[[ .SplunkHome ]]/certs" - state: directory - owner: splunk - group: splunk - mode: "0700" - -- name: "Publish tls-combined.pem from Secret (0600)" - copy: - src: "[[ .SrcDir ]]/tls-combined.pem" - dest: "[[ .SplunkHome ]]/certs/tls-combined.pem" - remote_src: true - owner: splunk - group: splunk - mode: "0600" - when: "s_tls_combined.stat.exists" - -- name: "Publish tls-combined.pem from server.pem (0600)" - copy: - src: "[[ .ServerPEM ]]" - dest: "[[ .SplunkHome ]]/certs/tls-combined.pem" - remote_src: true - owner: splunk - group: splunk - mode: "0600" - when: "not s_tls_combined.stat.exists" - -# ================= splunkd TLS (server.conf) ================= -- name: "Set [sslConfig] for splunkd (baseline)" - blockinfile: - path: "[[ .SplunkHome ]]/etc/system/local/server.conf" - marker: "# {mark} splunk-operator tls" - create: true - owner: splunk - group: splunk - mode: "0644" - block: |- - [sslConfig] - enableSplunkdSSL = true - serverCert = [[ .ServerPEM ]] - requireClientCert = false + elif command -v openssl >/dev/null 2>&1; then + openssl rand -base64 24 + else + dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64 + fi + register: tls_key_password + +# If a fixed password file is provided, prefer it +- name: "Override password from [[ .KVPasswordFile ]] if present" + no_log: true + set_fact: + kv_clear_pass: "{{ lookup('file', '[[ .KVPasswordFile ]]') | trim }}" + when: '("[[ .KVPasswordFile ]]" | length) > 0' + ignore_errors: true -- name: "Set sslRootCAPath if CA exists" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/server.conf" - section: sslConfig - option: sslRootCAPath - value: "[[ .CACrt ]]" - create: true - no_extra_spaces: true - when: "s_ca_crt.stat.exists or s_chain_crt.stat.exists" +- name: "Set kv_clear_pass final" + no_log: true + set_fact: + kv_clear_pass: "{{ kv_clear_pass | default(tls_key_password.stdout) }}" -# Optional: set [general] serverName (FQDN) if provided -[[ if ne .ServerName "" ]] -- name: "Set [general] serverName (optional)" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/server.conf" - section: general - option: serverName - value: "[[ .ServerName ]]" - create: true - no_extra_spaces: true - owner: splunk - group: splunk - mode: "0644" -[[ end ]] +# ================= Encrypt key and build bundles ================= +- name: "Encrypt private key -> tls_enc.key" + no_log: true + shell: | + set -e + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl rsa -aes256 \ + -passout pass:"{{ kv_clear_pass }}" \ + -in "[[ .TLSKey ]]" \ + -out "[[ .CanonicalDir ]]/tls_enc.key" + chown splunk:splunk "[[ .CanonicalDir ]]/tls_enc.key" + chmod 0600 "[[ .CanonicalDir ]]/tls_enc.key" + +- name: "Build KVBUNDLE at [[ .KVBundlePath ]] (enc key + full chain + CA if any)" + shell: | + set -e + cd "[[ .CanonicalDir ]]" + CHAIN_FILES="" + for f in S-*; do + [ -s "$f" ] && CHAIN_FILES="$CHAIN_FILES $f" + done + if [ -s "[[ .CACrt ]]" ]; then + cat tls_enc.key $CHAIN_FILES "[[ .CACrt ]]" > "[[ .KVBundlePath ]]" + else + cat tls_enc.key $CHAIN_FILES > "[[ .KVBundlePath ]]" + fi + chown splunk:splunk "[[ .KVBundlePath ]]" + chmod 0600 "[[ .KVBundlePath ]]" + rm -f S-* + +# Optional aliases for KVBUNDLE (customer names) +- name: "Set KVBUNDLE alias paths" + set_fact: + _alias_crt: "[[ .KVBundleAliasCRT ]]" + _alias_pem: "[[ .KVBundleAliasPEM ]]" -# ================= Splunk Web TLS (web.conf) ================= -- name: "Ensure web.conf [settings] startwebserver" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - section: settings - option: startwebserver - value: "1" - backup: false - create: true - no_extra_spaces: true +- name: "Alias to CRT path" + file: + src: "[[ .KVBundlePath ]]" + dest: "{{ _alias_crt }}" + state: link owner: splunk group: splunk - mode: "0644" - -- name: "Ensure web.conf [settings] httpport" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - section: settings - option: httpport - value: "8000" - no_extra_spaces: true - -- name: "Ensure web.conf [settings] enableSplunkWebSSL" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - section: settings - option: enableSplunkWebSSL - value: "1" - no_extra_spaces: true - -- name: "Ensure web.conf [settings] privKeyPath -> tls.key" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - section: settings - option: privKeyPath - value: "[[ .TLSKey ]]" - no_extra_spaces: true - -- name: "Ensure web.conf [settings] serverCert -> tls.crt" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - section: settings - option: serverCert - value: "[[ .TLSCrt ]]" - no_extra_spaces: true - -- name: "Ensure web.conf [settings] caCertPath -> ca.crt (if CA exists)" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - section: settings - option: caCertPath - value: "[[ .CACrt ]]" - no_extra_spaces: true - when: "s_ca_crt.stat.exists or s_chain_crt.stat.exists" - -# Defensive: remove an accidental serverCert=ca.crt line if present -- name: "Remove incorrect serverCert=ca.crt assignment (defensive)" - lineinfile: - path: "[[ .SplunkHome ]]/etc/system/local/web.conf" - regexp: '^\s*serverCert\s*=\s*/opt/splunk/etc/auth/tls/ca\.crt\s*$' - state: absent - -# Optional cleanup of invalid inputs.conf http sslRootCAPath -- name: "Stat inputs.conf" - stat: - path: "[[ .SplunkHome ]]/etc/system/local/inputs.conf" - register: s_inputs - -- name: "Remove sslRootCAPath from [http] in inputs.conf if present" - ini_file: - path: "[[ .SplunkHome ]]/etc/system/local/inputs.conf" - section: http - option: sslRootCAPath - state: absent - create: false - no_extra_spaces: true - when: "s_inputs.stat.exists" - -# ================= KV-store encrypted key (optional) ================= -# Turn logs on when debugging: ansible-playbook ... -e kv_nolog=false -e kv_emit_pass=false -# In normal runs, secrets stay hidden. -[[ if .KVEnable ]] -- name: "KV: build encrypted bundle and write [kvstore] (idempotent)" - no_log: "false" - ansible.builtin.shell: - cmd: | - set -e - - SPLUNK_HOME="[[ .SplunkHome ]]" - CANON="[[ .CanonicalDir ]]" - TLSKEY="[[ .TLSKey ]]" - TLSCRT="[[ .TLSCrt ]]" - CACRT="[[ .CACrt ]]" - KVBUNDLE="[[ .KVBundlePath ]]" - PASSFILE="[[ .KVPasswordFile ]]" - - # ---- choose a working openssl (prefer Splunk's with proper env) ---- - # choose a working openssl (prefer Splunk's with proper env) - OPENSSL="" - USE_SPLUNK_OPENSSL=0 - if [ -x /opt/splunk/bin/openssl ] && \ - LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ - /opt/splunk/bin/openssl version >/dev/null 2>&1; then - OPENSSL=/opt/splunk/bin/openssl - USE_SPLUNK_OPENSSL=1 - elif command -v openssl >/dev/null 2>&1; then - OPENSSL="$(command -v openssl)" - USE_SPLUNK_OPENSSL=0 - else - echo "ERROR: no usable openssl found" >&2 - exit 1 - fi - - ossl() { - if [ "$USE_SPLUNK_OPENSSL" = "1" ]; then - # Prefix env **as separate words** before the command - LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ - "$OPENSSL" "$@" - else - "$OPENSSL" "$@" - fi - } - # ------------------------------------------------------------------- - - # passphrase: Secret file if provided, else random - if [ -n "$PASSFILE" ] && [ -f "$PASSFILE" ]; then - PASS="$(tr -d '\r\n' < "$PASSFILE")" - else - PASS="$(ossl rand -base64 24)" - fi - - # encrypted key for KV - ossl rsa -aes256 -passout pass:"$PASS" -in "$TLSKEY" -out "$CANON/tls_enc.key" - chown splunk:splunk "$CANON/tls_enc.key" - chmod 0600 "$CANON/tls_enc.key" - - # KV bundle (enc key + cert + optional CA) - if [ -f "$CACRT" ] && [ -s "$CACRT" ]; then - cat "$CANON/tls_enc.key" "$TLSCRT" "$CACRT" > "$KVBUNDLE" - else - cat "$CANON/tls_enc.key" "$TLSCRT" > "$KVBUNDLE" - fi - chown splunk:splunk "$KVBUNDLE" - chmod 0600 "$KVBUNDLE" - - # Idempotently upsert [kvstore] sslPassword/serverCert - SC="$SPLUNK_HOME/etc/system/local/server.conf" - TMP="$SC.tmp.$$" - mkdir -p "$(dirname "$SC")" - touch "$SC" - chown splunk:splunk "$SC" - chmod 0644 "$SC" - - awk -v pass="$PASS" -v bundle="$KVBUNDLE" ' - BEGIN{ in=0; saw1=0; saw2=0 } - function emit(){ - if(!saw1) print "sslPassword = " pass - if(!saw2) print "serverCert = " bundle - } - { - if($0 ~ /^\[kvstore\]/){ - if(in){emit()} - print; in=1; saw1=0; saw2=0; next - } - if(in){ - if($0 ~ /^\[/){ emit(); in=0 } - else { - if($0 ~ /^[ \t]*sslPassword[ \t]*=/){ print "sslPassword = " pass; saw1=1; next } - if($0 ~ /^[ \t]*serverCert[ \t]*=/){ print "serverCert = " bundle; saw2=1; next } - } - } - print - } - END{ - if(in){emit()} else { - print "[kvstore]" - print "sslPassword = " pass - print "serverCert = " bundle - } - } - ' "$SC" > "$TMP" && mv "$TMP" "$SC" - - chown splunk:splunk "$SC" - chmod 0644 "$SC" - - # Emit the pass on one line so we can capture it (optional; turn off with -e kv_emit_pass=false) - if [ "${kv_emit_pass:-true}" = "true" ]; then - printf '__KV_PASS__:%s\n' "$PASS" - fi - executable: /bin/sh - environment: - # Let the script see the toggle (so it won’t print secrets when you set kv_emit_pass=false) - kv_emit_pass: "{{ kv_emit_pass | default(true) }}" - register: kv_build -[[ end ]] - + force: yes + when: _alias_crt | length > 0 -# ================= Overlay into /opt/splunk/auth (Option B) ================= -- name: "Ensure /opt/splunk/auth exists (real dir)" +- name: "Alias to PEM path" file: - path: /opt/splunk/auth - state: directory + src: "[[ .KVBundlePath ]]" + dest: "{{ _alias_pem }}" + state: link owner: splunk group: splunk - mode: "0755" - -# --- stat each auth target separately (no Jinja) --- -- name: "Auth overlay: stat /opt/splunk/auth/tls.key" - stat: - path: /opt/splunk/auth/tls.key - register: s_auth_tls_key - -- name: "Auth overlay: stat /opt/splunk/auth/tls.crt" - stat: - path: /opt/splunk/auth/tls.crt - register: s_auth_tls_crt - -- name: "Auth overlay: stat /opt/splunk/auth/ca.crt" - stat: - path: /opt/splunk/auth/ca.crt - register: s_auth_ca_crt - -- name: "Auth overlay: stat /opt/splunk/auth/server.pem" - stat: - path: /opt/splunk/auth/server.pem - register: s_auth_server_pem - -# Optional extras, only relevant when KV is enabled -[[ if .KVEnable ]] -- name: "Auth overlay: stat /opt/splunk/auth/kvstore.pem" - stat: - path: /opt/splunk/auth/kvstore.pem - register: s_auth_kvstore_pem - -- name: "Auth overlay: stat /opt/splunk/auth/splunk-bundle.crt" - stat: - path: /opt/splunk/auth/splunk-bundle.crt - register: s_auth_bundle_crt - -- name: "Auth overlay: stat /opt/splunk/auth/splunk-bundle-pass.crt" - stat: - path: /opt/splunk/auth/splunk-bundle-pass.crt - register: s_auth_bundle_pass_crt + force: yes + when: _alias_pem | length > 0 -# Extract the PASS from stdout; this keeps the value out of logs -- name: Extract KV password from kv_build output - no_log: true +# Pick the cert path your downstream roles expect (prefer customer-style alias) +- name: "Choose kv bundle cert path for facts" set_fact: - kv_pass: >- - {{ (kv_build.stdout | default('') ) - | regex_search('__KV_PASS__:(?P

.+)$', '\g

', multiline=True) | trim }} + kv_bundle_cert_path: >- + {{ _alias_crt if (_alias_crt | default('') | length > 0) else '[[ .KVBundlePath ]]' }} -# Build the splunk_ssl_password structure (your example, just Go/Jinja harmonized) -- name: Build splunk_ssl_password fact +# ===================== FACTS ONLY (no direct conf writes) ===================== +- name: "Build splunk_ssl_password fact" no_log: true set_fact: splunk_ssl_password: ssl: - password: "{{ kv_pass }}" - ca: "/opt/splunk/etc/auth/ca.crt" - cert: "/opt/splunk/etc/auth/splunk-bundle-pass.crt" + password: "{{ kv_clear_pass }}" + ca: "[[ .CACrt ]]" + cert: "{{ kv_bundle_cert_path }}" enable: true conf: - key: "server" value: - directory: "/opt/splunk/etc/system/local" + directory: "[[ .SplunkHome ]]/etc/system/local" content: kvstore: - sslPassword: "{{ kv_pass }}" - serverCert: "/opt/splunk/etc/auth/splunk-bundle-pass.crt" + serverCert: "{{ kv_bundle_cert_path }}" + sslPassword: "{{ kv_clear_pass }}" sslVerifyServerName: true + sslVerifyServerCert: true sslConfig: sslVersions: "tls1.2" sslVersionsForClient: "tls1.2" @@ -466,182 +255,34 @@ cliVerifyServerName: true sslVerifyServerCert: true caTrustStore: "splunk,OS" - caTrustStorePath: "/opt/splunk/etc/auth/custom-ca-bundle.crt" + caTrustStorePath: "[[ .CABundle ]]" node_auth: signatureVersion: "v2" -# Merge into splunk (recursive, append_rp for lists) -- name: Merge splunk_ssl_password into splunk +- name: "Merge splunk_ssl_password into splunk" no_log: true set_fact: splunk: "{{ (splunk | default({})) | combine(splunk_ssl_password, recursive=true, list_merge='append_rp') }}" -[[ end ]] - -# --- create missing links (no Jinja) --- -- name: "Auth overlay: create link tls.key" - file: - src: "[[ .TLSKey ]]" - dest: "/opt/splunk/auth/tls.key" - state: link - owner: splunk - group: splunk - when: "not s_auth_tls_key.stat.exists" - -- name: "Auth overlay: create link tls.crt" - file: - src: "[[ .TLSCrt ]]" - dest: "/opt/splunk/auth/tls.crt" - state: link - owner: splunk - group: splunk - when: "not s_auth_tls_crt.stat.exists" - -- name: "Auth overlay: create link ca.crt" - file: - src: "[[ .CACrt ]]" - dest: "/opt/splunk/auth/ca.crt" - state: link - owner: splunk - group: splunk - when: "not s_auth_ca_crt.stat.exists" - -- name: "Auth overlay: create link server.pem" - file: - src: "[[ .ServerPEM ]]" - dest: "/opt/splunk/auth/server.pem" - state: link - owner: splunk - group: splunk - when: "not s_auth_server_pem.stat.exists" - -[[ if .KVEnable ]] -- name: "Auth overlay: create link kvstore.pem" - file: - src: "[[ .KVBundlePath ]]" - dest: "/opt/splunk/auth/kvstore.pem" - state: link - owner: splunk - group: splunk - when: "not s_auth_kvstore_pem.stat.exists" - -- name: "Auth overlay: create link splunk-bundle.crt" - file: - src: "[[ .ServerPEM ]]" - dest: "/opt/splunk/auth/splunk-bundle.crt" - state: link - owner: splunk - group: splunk - when: "not s_auth_bundle_crt.stat.exists" - -- name: "Auth overlay: create link splunk-bundle-pass.crt" - file: - src: "[[ .KVBundlePath ]]" - dest: "/opt/splunk/auth/splunk-bundle-pass.crt" - state: link - owner: splunk - group: splunk - when: "not s_auth_bundle_pass_crt.stat.exists" -[[ end ]] - -# --- retarget wrong symlinks (no Jinja) --- -- name: "Auth overlay: retarget tls.key if wrong" - file: - src: "[[ .TLSKey ]]" - dest: "/opt/splunk/auth/tls.key" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_tls_key.stat.islnk | default(false) and (s_auth_tls_key.stat.lnk_source | default('')) != '[[ .TLSKey ]]'" - -- name: "Auth overlay: retarget tls.crt if wrong" - file: - src: "[[ .TLSCrt ]]" - dest: "/opt/splunk/auth/tls.crt" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_tls_crt.stat.islnk | default(false) and (s_auth_tls_crt.stat.lnk_source | default('')) != '[[ .TLSCrt ]]'" - -- name: "Auth overlay: retarget ca.crt if wrong" - file: - src: "[[ .CACrt ]]" - dest: "/opt/splunk/auth/ca.crt" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_ca_crt.stat.islnk | default(false) and (s_auth_ca_crt.stat.lnk_source | default('')) != '[[ .CACrt ]]'" - -- name: "Auth overlay: retarget server.pem if wrong" - file: - src: "[[ .ServerPEM ]]" - dest: "/opt/splunk/auth/server.pem" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_server_pem.stat.islnk | default(false) and (s_auth_server_pem.stat.lnk_source | default('')) != '[[ .ServerPEM ]]'" - -[[ if .KVEnable ]] -- name: "Auth overlay: retarget kvstore.pem if wrong" - file: - src: "[[ .KVBundlePath ]]" - dest: "/opt/splunk/auth/kvstore.pem" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_kvstore_pem.stat.islnk | default(false) and (s_auth_kvstore_pem.stat.lnk_source | default('')) != '[[ .KVBundlePath ]]'" - -- name: "Auth overlay: retarget splunk-bundle.crt if wrong" - file: - src: "[[ .ServerPEM ]]" - dest: "/opt/splunk/auth/splunk-bundle.crt" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_bundle_crt.stat.islnk | default(false) and (s_auth_bundle_crt.stat.lnk_source | default('')) != '[[ .ServerPEM ]]'" - -- name: "Auth overlay: retarget splunk-bundle-pass.crt if wrong" - file: - src: "[[ .KVBundlePath ]]" - dest: "/opt/splunk/auth/splunk-bundle-pass.crt" - state: link - owner: splunk - group: splunk - force: yes - when: "s_auth_bundle_pass_crt.stat.islnk | default(false) and (s_auth_bundle_pass_crt.stat.lnk_source | default('')) != '[[ .KVBundlePath ]]'" -[[ end ]] - # ================= Final safety: assert canonical perms ================= -- name: "Re-assert canonical file perms" +- name: "Re-assert perms ([[ .TLSKey ]])" file: path: "[[ .TLSKey ]]" owner: splunk group: splunk mode: "0600" -- name: "Re-assert canonical file perms (tls.crt)" +- name: "Re-assert perms ([[ .TLSCrt ]])" file: path: "[[ .TLSCrt ]]" owner: splunk group: splunk mode: "0644" -- name: "Re-assert canonical file perms (server.pem)" - file: - path: "[[ .ServerPEM ]]" - owner: splunk - group: splunk - mode: "0600" - -- name: "Re-assert canonical file perms (ca.crt)" +- name: "Re-assert perms ([[ .CACrt ]])" file: path: "[[ .CACrt ]]" owner: splunk group: splunk mode: "0644" + when: s_ca_crt.stat.exists diff --git a/pkg/tls/validate.go b/pkg/tls/validate.go index 355e8d34d..fc9320551 100644 --- a/pkg/tls/validate.go +++ b/pkg/tls/validate.go @@ -3,19 +3,19 @@ package tls import ( - "fmt" - v4 "github.com/splunk/splunk-operator/api/v4" + "fmt" + v4 "github.com/splunk/splunk-operator/api/v4" ) func ValidateTLSSpec(t *v4.TLSConfig) error { - if t == nil { - return nil - } - if t.KVEncryptedKey != nil && t.KVEncryptedKey.Enabled { - // nothing hard to validate; optional SecretKeySelector is fine - if t.CanonicalDir == "" { - return fmt.Errorf("tls.canonicalDir is required when kvEncryptedKey.enabled=true") - } - } - return nil + if t == nil { + return nil + } + if t.KVEncryptedKey != nil && t.KVEncryptedKey.Enabled { + // nothing hard to validate; optional SecretKeySelector is fine + if t.CanonicalDir == "" { + return fmt.Errorf("tls.canonicalDir is required when kvEncryptedKey.enabled=true") + } + } + return nil } From f893387131ef2a932849df3da338ea8d5c9a6172 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Sun, 5 Oct 2025 11:29:52 -0700 Subject: [PATCH 4/5] working pretask tmpl --- pkg/tls/templates/pretasks.tmpl.yaml | 293 +++++++++++++-------------- 1 file changed, 139 insertions(+), 154 deletions(-) diff --git a/pkg/tls/templates/pretasks.tmpl.yaml b/pkg/tls/templates/pretasks.tmpl.yaml index 9ad9b6500..b083002e4 100644 --- a/pkg/tls/templates/pretasks.tmpl.yaml +++ b/pkg/tls/templates/pretasks.tmpl.yaml @@ -26,6 +26,11 @@ path: "[[ .SrcDir ]]/ca.crt" register: s_ca_crt + - name: "Stat trust-manager bundle (if any)" + stat: + path: "[[ .TrustDir ]]/[[ .TrustKey ]]" + register: s_trust_mgr + # Fail early if key/cert missing - name: "Validate required source files are present" assert: @@ -35,8 +40,8 @@ fail_msg: "TLS inputs not found at [[ .SrcDir ]]. Mount Secret/CSI with tls.crt and tls.key." success_msg: "Found tls.crt and tls.key at [[ .SrcDir ]]" -# Copy into canonical paths (explicit target filenames) -- name: "Copy tls.crt -> [[ .TLSCrt ]]" +# Copy into canonical paths +- name: "Copy tls.crt -> canonical" copy: src: "[[ .SrcDir ]]/tls.crt" dest: "[[ .TLSCrt ]]" @@ -46,7 +51,7 @@ remote_src: true when: s_tls_crt.stat.exists -- name: "Copy tls.key -> [[ .TLSKey ]] (0600)" +- name: "Copy tls.key -> canonical (0600)" copy: src: "[[ .SrcDir ]]/tls.key" dest: "[[ .TLSKey ]]" @@ -56,7 +61,7 @@ remote_src: true when: s_tls_key.stat.exists -- name: "Copy ca.crt -> [[ .CACrt ]] (if present)" +- name: "Copy CA to canonical if present" copy: src: "[[ .SrcDir ]]/ca.crt" dest: "[[ .CACrt ]]" @@ -66,176 +71,144 @@ remote_src: true when: s_ca_crt.stat.exists -# ================= Optional trust bundle (from separate Secret) ================= -- name: "Optional trust bundle from [[ .TrustDir ]]/[[ .TrustKey ]] -> [[ .CABundle ]]" - block: - - name: "Stat trust bundle source" - stat: - path: "[[ .TrustDir ]]/[[ .TrustKey ]]" - register: s_bundle - - - name: "Copy provided trust bundle" - copy: - src: "[[ .TrustDir ]]/[[ .TrustKey ]]" - dest: "[[ .CABundle ]]" - owner: splunk - group: splunk - mode: "0644" - remote_src: true - when: s_bundle.stat.exists +# ================= Build server.pem (splunkd cert bundle) ================= +- name: Build server.pem (key + cert + optional CA) + shell: + cmd: /bin/sh -s + stdin: |- + set -e + if [ -f "[[ .TLSCrt ]]" ] && [ -s "[[ .CACrt ]]" ]; then + cat "[[ .TLSKey ]]" "[[ .TLSCrt ]]" "[[ .CACrt ]]" > "[[ .ServerPEM ]]" + else + cat "[[ .TLSKey ]]" "[[ .TLSCrt ]]" > "[[ .ServerPEM ]]" + fi + chown splunk:splunk "[[ .ServerPEM ]]" + chmod 0600 "[[ .ServerPEM ]]" -# ================= Build custom trust store (customer behavior) ================= -- name: "Detect OS CA bundle path" - shell: | - for f in \ - /etc/pki/tls/certs/ca-bundle.crt \ - /etc/ssl/certs/ca-certificates.crt \ - /etc/ssl/ca-bundle.pem \ - /etc/ssl/cert.pem - do - [ -r "$f" ] && { echo "$f"; exit 0; } - done - exit 1 - register: _os_ca - changed_when: false - failed_when: false +# ================= Build TRUST BUNDLE ================= +# Result path is [[ .CABundle ]] (recommend: /opt/splunk/etc/auth/custom-ca-bundle.crt) +# 1) Find a system CA bundle (pick the first that exists) +- name: Detect system CA bundle path + stat: + path: "{{ item }}" + loop: + - /etc/pki/tls/certs/ca-bundle.crt + - /etc/ssl/certs/ca-bundle.crt + - /etc/ssl/cert.pem + register: sysca_candidates -- name: "Build trust bundle at [[ .CABundle ]] (CA + OS bundle if present)" - shell: | - set -e - dst="[[ .CABundle ]]" - ca="[[ .CACrt ]]" - os="{{ _os_ca.stdout | default('') }}" - tmp="$(mktemp)" - if [ -s "$ca" ] && [ -n "$os" ] && [ -s "$os" ]; then - cat "$ca" "$os" > "$tmp" - elif [ -s "$ca" ]; then - cat "$ca" > "$tmp" - elif [ -n "$os" ] && [ -s "$os" ]; then - cat "$os" > "$tmp" - else - # nothing to build - exit 0 - fi - install -o splunk -g splunk -m 0644 -T "$tmp" "$dst" - rm -f "$tmp" - when: not (s_bundle.stat.exists | default(false)) +- name: Set system_ca_path fact + set_fact: + system_ca_path: >- + {{ (sysca_candidates.results + | selectattr('stat.exists') + | map(attribute='stat.path') + | list | first) | default('', true) }} -# ================= Split tls.crt into S-* (leaf + chain) ================= -- name: "Split [[ .TLSCrt ]] into S-* (leaf + chain)" - shell: | - set -e - cd "[[ .CanonicalDir ]]" - rm -f S-* || true - awk 'BEGIN{n=-1} - /-----BEGIN CERTIFICATE-----/{n++; file=sprintf("S-%02d",n)} - {if(n>=0) print > file} - /-----END CERTIFICATE-----/{close(file)}' "[[ .TLSCrt ]]" - test -s S-00 +# Assemble merged trust store -> **[[ .CABundle ]]** (canonical) +- name: Assemble merged trust store (Secret + trust-manager + system) + shell: + cmd: /bin/sh -s + stdin: | + set -e + OUT="[[ .CABundle ]]" # e.g., /opt/splunk/etc/auth/trust-bundle.crt + TMP="$(mktemp)" + # Append in this order: Secret CA, trust-manager bundle, system CA + for f in "[[ .CACrt ]]" "[[ .CABundle ]]" "{{ system_ca_path | default('') }}"; do + if [ -n "$f" ] && [ -s "$f" ] && [ "$f" != "$OUT" ]; then + cat "$f" >>"$TMP" + printf '\n' >>"$TMP" + fi + done + # Write atomically with ownership/perm + install -D -o splunk -g splunk -m 0644 -T "$TMP" "$OUT" + rm -f "$TMP" changed_when: true -# ================= Generate password (prefer Splunk OpenSSL; fallback safe) ================= -- name: "Generate kv/tls key password" + +# ================= KV encrypted key + bundles ================= +- name: Generate KV passphrase (prefer Splunk OpenSSL, else system, else dd) no_log: true - shell: | - set -e - if LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" /opt/splunk/bin/openssl version >/dev/null 2>&1; then - if LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" /opt/splunk/bin/openssl rand -base64 24 >/dev/null 2>&1; then - LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" /opt/splunk/bin/openssl rand -base64 24 - else - dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64 + ansible.builtin.shell: + cmd: /bin/sh -s + stdin: | + set -e + # Try Splunk's OpenSSL first (with proper env), then system openssl, then dd + if [ -x /opt/splunk/bin/openssl ] && \ + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl version >/dev/null 2>&1; then + if LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl rand -base64 24 >/dev/null 2>&1; then + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl rand -base64 24 + exit 0 + fi + fi + if command -v openssl >/dev/null 2>&1 && \ + openssl rand -base64 24 >/dev/null 2>&1; then + openssl rand -base64 24 + exit 0 fi - elif command -v openssl >/dev/null 2>&1; then - openssl rand -base64 24 - else dd if=/dev/urandom bs=24 count=1 2>/dev/null | base64 - fi - register: tls_key_password - -# If a fixed password file is provided, prefer it -- name: "Override password from [[ .KVPasswordFile ]] if present" - no_log: true - set_fact: - kv_clear_pass: "{{ lookup('file', '[[ .KVPasswordFile ]]') | trim }}" - when: '("[[ .KVPasswordFile ]]" | length) > 0' - ignore_errors: true + register: kv_pass + changed_when: true -- name: "Set kv_clear_pass final" - no_log: true - set_fact: - kv_clear_pass: "{{ kv_clear_pass | default(tls_key_password.stdout) }}" -# ================= Encrypt key and build bundles ================= -- name: "Encrypt private key -> tls_enc.key" +- name: "Create encrypted private key for KV (tls_enc.key)" no_log: true shell: | set -e - LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ - /opt/splunk/bin/openssl rsa -aes256 \ - -passout pass:"{{ kv_clear_pass }}" \ - -in "[[ .TLSKey ]]" \ - -out "[[ .CanonicalDir ]]/tls_enc.key" + PASS="{{ kv_pass.stdout | trim }}" + if [ -x /opt/splunk/bin/openssl ] && \ + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl version >/dev/null 2>&1; then + LD_LIBRARY_PATH=/opt/splunk/lib OPENSSL_FIPS="${SPLUNK_FIPS:-}" \ + /opt/splunk/bin/openssl rsa -aes256 -passout pass:"$PASS" \ + -in "[[ .TLSKey ]]" -out "[[ .CanonicalDir ]]/tls_enc.key" + else + openssl rsa -aes256 -passout pass:"$PASS" \ + -in "[[ .TLSKey ]]" -out "[[ .CanonicalDir ]]/tls_enc.key" + fi chown splunk:splunk "[[ .CanonicalDir ]]/tls_enc.key" chmod 0600 "[[ .CanonicalDir ]]/tls_enc.key" -- name: "Build KVBUNDLE at [[ .KVBundlePath ]] (enc key + full chain + CA if any)" +- name: "Build KV bundle(s): kvstore.pem + splunk-bundle-pass.crt + splunk-bundle.crt" + no_log: false shell: | set -e - cd "[[ .CanonicalDir ]]" - CHAIN_FILES="" - for f in S-*; do - [ -s "$f" ] && CHAIN_FILES="$CHAIN_FILES $f" - done - if [ -s "[[ .CACrt ]]" ]; then - cat tls_enc.key $CHAIN_FILES "[[ .CACrt ]]" > "[[ .KVBundlePath ]]" + CAN="[[ .CanonicalDir ]]" + TLSCRT="[[ .TLSCrt ]]" + CACRT="[[ .CACrt ]]" + + # Encrypted bundle for KV (enc key + cert + optional CA) + if [ -s "$CACRT" ]; then + cat "$CAN/tls_enc.key" "$TLSCRT" "$CACRT" > "[[ .KVBundlePath ]]" + cat "$TLSCRT" "[[ .CACrt ]]" > "$CAN/tls.fullchain.crt" else - cat tls_enc.key $CHAIN_FILES > "[[ .KVBundlePath ]]" + cat "$CAN/tls_enc.key" "$TLSCRT" > "[[ .KVBundlePath ]]" + cp -f "$TLSCRT" "$CAN/tls.fullchain.crt" fi chown splunk:splunk "[[ .KVBundlePath ]]" chmod 0600 "[[ .KVBundlePath ]]" - rm -f S-* - -# Optional aliases for KVBUNDLE (customer names) -- name: "Set KVBUNDLE alias paths" - set_fact: - _alias_crt: "[[ .KVBundleAliasCRT ]]" - _alias_pem: "[[ .KVBundleAliasPEM ]]" - -- name: "Alias to CRT path" - file: - src: "[[ .KVBundlePath ]]" - dest: "{{ _alias_crt }}" - state: link - owner: splunk - group: splunk - force: yes - when: _alias_crt | length > 0 - -- name: "Alias to PEM path" - file: - src: "[[ .KVBundlePath ]]" - dest: "{{ _alias_pem }}" - state: link - owner: splunk - group: splunk - force: yes - when: _alias_pem | length > 0 -# Pick the cert path your downstream roles expect (prefer customer-style alias) -- name: "Choose kv bundle cert path for facts" - set_fact: - kv_bundle_cert_path: >- - {{ _alias_crt if (_alias_crt | default('') | length > 0) else '[[ .KVBundlePath ]]' }} + ln -sf "[[ .KVBundlePath ]]" "$CAN/splunk-bundle-pass.crt" + if [ -s "$CACRT" ]; then + cat "[[ .TLSKey ]]" "$TLSCRT" "$CACRT" > "$CAN/splunk-bundle.crt" + else + cat "[[ .TLSKey ]]" "$TLSCRT" > "$CAN/splunk-bundle.crt" + fi + chown splunk:splunk "$CAN/splunk-bundle.crt" + chmod 0600 "$CAN/splunk-bundle.crt" -# ===================== FACTS ONLY (no direct conf writes) ===================== -- name: "Build splunk_ssl_password fact" +# =================== Export Ansible facts (no direct conf writes) =================== +- name: "Set splunk_ssl_password fact (KV + trust store)" no_log: true set_fact: splunk_ssl_password: ssl: - password: "{{ kv_clear_pass }}" - ca: "[[ .CACrt ]]" - cert: "{{ kv_bundle_cert_path }}" + password: "{{ kv_pass.stdout | trim }}" + ca: "[[ .CACrt ]]" # leaf CA (from Secret) + cert: "[[ .CanonicalDir ]]/splunk-bundle-pass.crt" # encrypted bundle (enc key + chain) enable: true conf: - key: "server" @@ -243,8 +216,8 @@ directory: "[[ .SplunkHome ]]/etc/system/local" content: kvstore: - serverCert: "{{ kv_bundle_cert_path }}" - sslPassword: "{{ kv_clear_pass }}" + sslPassword: "{{ kv_pass.stdout | trim }}" + serverCert: "[[ .KVBundlePath ]]" # we built this for KV: encrypted key + chain sslVerifyServerName: true sslVerifyServerCert: true sslConfig: @@ -255,7 +228,7 @@ cliVerifyServerName: true sslVerifyServerCert: true caTrustStore: "splunk,OS" - caTrustStorePath: "[[ .CABundle ]]" + caTrustStorePath: "[[ .CABundle ]]" # merged bundle we created above node_auth: signatureVersion: "v2" @@ -265,24 +238,36 @@ splunk: "{{ (splunk | default({})) | combine(splunk_ssl_password, recursive=true, list_merge='append_rp') }}" # ================= Final safety: assert canonical perms ================= -- name: "Re-assert perms ([[ .TLSKey ]])" +# Re-assert perms only if the merged bundle exists +- name: Stat merged trust bundle + stat: + path: "[[ .CABundle ]]" + register: s_trust + +- name: "Re-assert canonical file perms (tls.key)" file: path: "[[ .TLSKey ]]" owner: splunk group: splunk mode: "0600" -- name: "Re-assert perms ([[ .TLSCrt ]])" +- name: "Re-assert canonical file perms (tls.crt)" file: path: "[[ .TLSCrt ]]" owner: splunk group: splunk mode: "0644" -- name: "Re-assert perms ([[ .CACrt ]])" +- name: "Re-assert canonical file perms (server.pem)" file: - path: "[[ .CACrt ]]" + path: "[[ .ServerPEM ]]" + owner: splunk + group: splunk + mode: "0600" + +- name: "Re-assert canonical file perms (merged trust bundle)" + file: + path: "[[ .CABundle ]]" owner: splunk group: splunk mode: "0644" - when: s_ca_crt.stat.exists From 7a7707615b20d0d8511cb6bfc30a04eb1b77c5a1 Mon Sep 17 00:00:00 2001 From: Vivek Reddy Date: Sun, 5 Oct 2025 12:58:28 -0700 Subject: [PATCH 5/5] adding defaults for each splunk roles, these are idempotent configuration --- pkg/splunk/enterprise/configuration.go | 7 + pkg/splunk/enterprise/default_roles.go | 493 +++++++++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 pkg/splunk/enterprise/default_roles.go diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index a0d90b354..47d27635c 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -909,6 +909,11 @@ func updateSplunkPodTemplateWithConfig(ctx context.Context, client splcommon.Con readinessProbe := getReadinessProbe(ctx, cr, instanceType, spec) startupProbe := getStartupProbe(ctx, cr, instanceType, spec) + rolePath, _, err := AddRoleDefaultsToPodTemplate(ctx, client, podTemplateSpec, cr, instanceType) + if err != nil { + return // bubble up or handle + } + // prepare defaults variable splunkDefaults := "/mnt/splunk-secrets/default.yml" // Check for apps defaults and add it to only the standalone or deployer/cm/mc instances @@ -921,6 +926,8 @@ func updateSplunkPodTemplateWithConfig(ctx context.Context, client splcommon.Con if spec.Defaults != "" { splunkDefaults = fmt.Sprintf("%s,%s", "/mnt/splunk-defaults/default.yml", splunkDefaults) } + // finally prepend the role baseline + splunkDefaults = PrependDefaultsURL(splunkDefaults, rolePath) // prepare container env variables role := instanceType.ToRole() diff --git a/pkg/splunk/enterprise/default_roles.go b/pkg/splunk/enterprise/default_roles.go new file mode 100644 index 000000000..e88a2bab6 --- /dev/null +++ b/pkg/splunk/enterprise/default_roles.go @@ -0,0 +1,493 @@ +// Copyright (c) 2018-2025 Splunk Inc. +// SPDX-License-Identifier: Apache-2.0 + +package enterprise + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" + splctrl "github.com/splunk/splunk-operator/pkg/splunk/splkcontroller" +) + +// ------------------------------- +// Role Defaults: names & content +// ------------------------------- + +// Well-known names so customers can find/manage them easily. +func GetRoleDefaultsConfigMapName(t InstanceType) string { + switch t { + case SplunkStandalone: + return "defaults-base-standalone" + case SplunkClusterManager, SplunkClusterMaster: + return "defaults-base-clustermanager" + case SplunkIndexer: + return "defaults-base-idx" + case SplunkSearchHead: + return "defaults-base-sh" + case SplunkMonitoringConsole: + return "defaults-base-mc" + case SplunkLicenseManager, SplunkLicenseMaster: + return "defaults-base-lm" + default: + return "defaults-base-generic" + } +} + +// SHA256 for seed tracking (for non-destructive upgrades when customers didn't edit). +func seedHash(b string) string { + h := sha256.Sum256([]byte(b)) + return hex.EncodeToString(h[:]) +} + +// Minimal, safe role defaults matching docker-splunk / Splunk-Ansible structure. +func builtinRoleDefaultsYAML(t InstanceType) string { + switch t { + case SplunkStandalone: + return baseStandalone + case SplunkClusterManager, SplunkClusterMaster: + return baseClusterManager + case SplunkIndexer: + return baseIndexer + case SplunkSearchHead: + return baseSearchHead + case SplunkMonitoringConsole: + return baseMonitoringConsole + case SplunkLicenseManager, SplunkLicenseMaster: + return baseLicense + default: + return baseGeneric + } +} + +// ------------------------------------------------------- +// EnsureRoleDefaultsConfigMap: create, or safe seed-upgrade +// ------------------------------------------------------- + +func EnsureRoleDefaultsConfigMap(ctx context.Context, client splcommon.ControllerClient, ns string, t InstanceType) (*corev1.ConfigMap, error) { + name := GetRoleDefaultsConfigMapName(t) + nn := types.NamespacedName{Namespace: ns, Name: name} + + cm, err := splctrl.GetConfigMap(ctx, client, nn) + builtin := builtinRoleDefaultsYAML(t) + newHash := seedHash(builtin) + + if err != nil { + if !k8serrors.IsNotFound(err) { + return nil, err + } + // Create with our seed (managed-by + seed-hash); customers can edit later. + cm = &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Labels: map[string]string{ + "app.kubernetes.io/managed-by": "splunk-operator", + }, + Annotations: map[string]string{ + "enterprise.splunk.com/seed-hash": newHash, + }, + }, + Data: map[string]string{ + "defaults.yaml": builtin, + }, + } + _, err = splctrl.ApplyConfigMap(ctx, client, cm) + return cm, err + } + + // Exists. Only refresh if we created it previously AND content is still the old seed. + lbl := cm.Labels["app.kubernetes.io/managed-by"] + oldHash := cm.Annotations["enterprise.splunk.com/seed-hash"] + current := cm.Data["defaults.yaml"] + + if lbl == "splunk-operator" && oldHash == seedHash(current) && oldHash != newHash { + if cm.Annotations == nil { + cm.Annotations = map[string]string{} + } + cm.Data["defaults.yaml"] = builtin + cm.Annotations["enterprise.splunk.com/seed-hash"] = newHash + _, err = splctrl.ApplyConfigMap(ctx, client, cm) + if err != nil { + return cm, err + } + } + + return cm, nil +} + +// ------------------------------------------------------------ +// Idempotent mount & chaining helpers +// ------------------------------------------------------------ + +// hasVolume returns true if a volume with name exists in the pod template. +func hasVolume(pod *corev1.PodTemplateSpec, name string) bool { + for _, v := range pod.Spec.Volumes { + if v.Name == name { + return true + } + } + return false +} + +// hasMount returns true if the container already mounts volume name at path. +func hasMount(c *corev1.Container, name, path string) bool { + for _, m := range c.VolumeMounts { + if m.Name == name && m.MountPath == path { + return true + } + } + return false +} + +// upsertEnv adds or updates the given env var on the container. +func upsertEnv(c *corev1.Container, key, val string) { + for i := range c.Env { + if c.Env[i].Name == key { + c.Env[i].Value = val + return + } + } + c.Env = append(c.Env, corev1.EnvVar{Name: key, Value: val}) +} + +// getContainerIndexByName finds the index of a container by name. +func getContainerIndexByName(pod *corev1.PodTemplateSpec, name string) (int, bool) { + for i := range pod.Spec.Containers { + if pod.Spec.Containers[i].Name == name { + return i, true + } + } + return -1, false +} + +// addVolumeToTemplateIdempotent adds a ConfigMap-backed volume + mount to ALL containers, idempotently. +// (We keep the signature general; callers can reuse for other files too.) +func addVolumeToTemplateIdempotent( + pod *corev1.PodTemplateSpec, + volName, mountPath string, + cmName, key, fileName string, + mode int32, +) { + if !hasVolume(pod, volName) { + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: volName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, + DefaultMode: &mode, + Items: []corev1.KeyToPath{{Key: key, Path: fileName, Mode: &mode}}, + }, + }, + }) + } + + for i := range pod.Spec.Containers { + if !hasMount(&pod.Spec.Containers[i], volName, mountPath) { + pod.Spec.Containers[i].VolumeMounts = append( + pod.Spec.Containers[i].VolumeMounts, + corev1.VolumeMount{Name: volName, MountPath: mountPath}, + ) + } + } +} + +// AddRoleDefaultsToPodTemplate mounts the role defaults CM at /mnt/role-defaults +// and annotates the *PodTemplateSpec* so edits recycle pods. +// Returns the full path to /mnt/role-defaults/defaults.yaml for convenience. +func AddRoleDefaultsToPodTemplate( + ctx context.Context, + client splcommon.ControllerClient, + podTemplate *corev1.PodTemplateSpec, + cr splcommon.MetaObject, + t InstanceType, +) (mountedPath string, cm *corev1.ConfigMap, err error) { + + cm, err = EnsureRoleDefaultsConfigMap(ctx, client, cr.GetNamespace(), t) + if err != nil { + return "", nil, err + } + + if podTemplate.ObjectMeta.Annotations == nil { + podTemplate.ObjectMeta.Annotations = map[string]string{} + } + + const ( + volName = "mnt-role-defaults" + mountDir = "/mnt/role-defaults" + key = "defaults.yaml" + fileName = "defaults.yaml" + ) + mode := int32(corev1.ConfigMapVolumeSourceDefaultMode) + + // Idempotent add (no duplicate volumes or mounts on repeated calls) + addVolumeToTemplateIdempotent(podTemplate, volName, mountDir, cm.GetName(), key, fileName, mode) + + // Change annotation when ConfigMap resourceVersion changes to trigger rollout + podTemplate.ObjectMeta.Annotations["roleDefaultsRev"] = cm.ResourceVersion + + return fmt.Sprintf("%s/%s", mountDir, fileName), cm, nil +} + +// WireRoleDefaultsIntoEnv prepends the mounted role defaults file to SPLUNK_DEFAULTS_URL +// on the "splunk" container (creating the env var if needed). +func WireRoleDefaultsIntoEnv(podTemplate *corev1.PodTemplateSpec, defaultsFilePath string) { + idx, ok := getContainerIndexByName(podTemplate, "splunk") + if !ok { + // If the "splunk" container is not present, apply to the first container. + if len(podTemplate.Spec.Containers) == 0 { + return + } + idx = 0 + } + + // Fetch current value (if any) then prepend ours (operator precedence). + cur := "" + for _, e := range podTemplate.Spec.Containers[idx].Env { + if e.Name == "SPLUNK_DEFAULTS_URL" { + cur = e.Value + break + } + } + newVal := PrependDefaultsURL(cur, defaultsFilePath) + upsertEnv(&podTemplate.Spec.Containers[idx], "SPLUNK_DEFAULTS_URL", newVal) +} + +// EnsureRoleDefaultsAndWire is a convenience wrapper that: +// 1) Ensures the role defaults ConfigMap exists (or seed-upgrades it), +// 2) Mounts it into the PodTemplateSpec, +// 3) Prepends the defaults file into SPLUNK_DEFAULTS_URL on the "splunk" container. +func EnsureRoleDefaultsAndWire( + ctx context.Context, + client splcommon.ControllerClient, + podTemplate *corev1.PodTemplateSpec, + cr splcommon.MetaObject, + t InstanceType, +) (*corev1.ConfigMap, error) { + + path, cm, err := AddRoleDefaultsToPodTemplate(ctx, client, podTemplate, cr, t) + if err != nil { + return nil, err + } + WireRoleDefaultsIntoEnv(podTemplate, path) + return cm, nil +} + +// PrependDefaultsURL mirrors operator behavior: put "extra" on the left. +func PrependDefaultsURL(existing, extra string) string { + if strings.TrimSpace(extra) == "" { + return existing + } + if strings.TrimSpace(existing) == "" { + return extra + } + return extra + "," + existing +} + +// -------------------------- +// Built-in YAML (safe base) +// -------------------------- + +const baseCommon = ` +hide_password: true +ansible_pre_tasks: "" +splunk: + http_enableSSL: 1 + http_enableSSL_cert: /opt/splunk/etc/auth/tls.crt + http_enableSSL_privKey: /opt/splunk/etc/auth/tls.key + launch: + PYTHONHTTPSVERIFY: 1 + SPLUNK_FIPS: 1 +` + +// Standalone +const baseStandalone = baseCommon + ` +splunk: + conf: + - key: server + value: + directory: /opt/splunk/etc/system/local + content: + general: + sessionTimeout: 5m + kvstore: + disabled: "true" + - key: inputs + value: + directory: /opt/splunk/etc/system/local + content: + http: + disabled: 1 + - key: web + value: + directory: /opt/splunk/etc/system/local + content: + settings: + ui_inactivity_timeout: 5 + tools.sessions.timeout: 5 +` + +// Cluster Manager / Master +const baseClusterManager = baseCommon + ` +splunk: + conf: + - key: server + value: + directory: /opt/splunk/etc/system/local + content: + general: + sessionTimeout: 5m + kvstore: + disabled: "true" + - key: inputs + value: + directory: /opt/splunk/etc/system/local + content: + http: + disabled: 1 + - key: web + value: + directory: /opt/splunk/etc/system/local + content: + settings: + ui_inactivity_timeout: 5 + tools.sessions.timeout: 5 + - key: indexes + value: + directory: /opt/splunk/etc/manager-apps/_cluster/local + content: + _introspection: + repFactor: auto + _metrics: + repFactor: auto + _metrics_rollup: + repFactor: auto + _configtracker: + repFactor: auto +` + +// Indexer +const baseIndexer = baseCommon + ` +splunk: + s2s: + ca: /opt/splunk/etc/auth/ca.crt + cert: /opt/splunk/etc/auth/splunk-bundle.crt + enable: "true" + port: 9997 + ssl: "true" + conf: + - key: server + value: + directory: /opt/splunk/etc/system/local + content: + general: + sessionTimeout: 5m + kvstore: + disabled: "true" + replication_port://9887: + disabled: "true" + replication_port-ssl://9887: + serverCert: /opt/splunk/etc/auth/splunk-bundle.crt + requireClientCert: "true" + - key: inputs + value: + directory: /opt/splunk/etc/system/local + content: + http: + disabled: 1 + SSL: + requireClientCert: "true" + - key: web + value: + directory: /opt/splunk/etc/system/local + content: + settings: + startwebserver: 0 +` + +// Search Head +const baseSearchHead = baseCommon + ` +splunk: + conf: + - key: server + value: + directory: /opt/splunk/etc/system/local + content: + general: + sessionTimeout: 5m + kvstore: + sslVerifyServerCert: "true" + replication_port://9887: + disabled: "true" + replication_port-ssl://9887: + serverCert: /opt/splunk/etc/auth/splunk-bundle.crt + requireClientCert: "true" + httpServer: + max_content_length: 5000000000 + - key: web + value: + directory: /opt/splunk/etc/system/local + content: + settings: + ui_inactivity_timeout: 5 + tools.sessions.timeout: 5 + max_upload_size: 2048 + splunkdConnectionTimeout: 300 +` + +// Monitoring Console +const baseMonitoringConsole = baseCommon + ` +splunk: + conf: + - key: server + value: + directory: /opt/splunk/etc/system/local + content: + general: + sessionTimeout: 5m + kvstore: + disabled: "true" + - key: inputs + value: + directory: /opt/splunk/etc/system/local + content: + http: + disabled: 1 + - key: web + value: + directory: /opt/splunk/etc/system/local + content: + settings: + ui_inactivity_timeout: 5 + tools.sessions.timeout: 5 +` + +// License Manager +const baseLicense = baseCommon + ` +splunk: + conf: + - key: server + value: + directory: /opt/splunk/etc/system/local + content: + general: + sessionTimeout: 5m + - key: inputs + value: + directory: /opt/splunk/etc/system/local + content: + http: + disabled: 1 +` + +// Fallback +const baseGeneric = baseCommon