diff --git a/cmd/main.go b/cmd/main.go index c3dd5dd4471..e04f6accdb6 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -312,6 +312,8 @@ func main() { Client: client, Scheme: mgr.GetScheme(), DisableDefaultInstall: strings.ToLower(os.Getenv(common.DisableDefaultInstallEnvVar)) == "true", + TLSMinVersion: string(profile.MinTLSVersion), + TLSCiphers: profile.Ciphers, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "GitopsService") os.Exit(1) diff --git a/controllers/consoleplugin.go b/controllers/consoleplugin.go index db27fa86f5c..76f1f741a49 100644 --- a/controllers/consoleplugin.go +++ b/controllers/consoleplugin.go @@ -2,10 +2,13 @@ package controllers import ( "context" + "crypto/md5" "fmt" + "io" "os" "reflect" "sort" + "strings" argocommon "github.com/argoproj-labs/argocd-operator/common" argocdutil "github.com/argoproj-labs/argocd-operator/controllers/argoutil" @@ -240,18 +243,45 @@ func securityContextForPlugin() *corev1.SecurityContext { } } -var httpdConfig = fmt.Sprintf(`LoadModule ssl_module modules/mod_ssl.so +func TLSVersionToPlugin(tlsVersion string) string { + versionMap := map[string]string{ + "VersionTLS12": "TLSv1.2", + "VersionTLS13": "TLSv1.3", + "VersionTLS11": "TLSv1.1", + "VersionTLS10": "TLSv1", + } + if v, ok := versionMap[tlsVersion]; ok { + return v + } + return "" // default fallback +} + +// buildHttpdConfig generates httpd.conf with dynamic TLS settings +func (r *ReconcileGitopsService) buildHttpdConfig() string { + httpdConfigBase := fmt.Sprintf(`LoadModule ssl_module modules/mod_ssl.so Listen %d https ServerRoot "/etc/httpd" - DocumentRoot /var/www/html/plugin SSLEngine on SSLCertificateFile "/etc/httpd-ssl/certs/tls.crt" - SSLCertificateKeyFile "/etc/httpd-ssl/private/tls.key" -`, servicePort, servicePort) + SSLCertificateKeyFile "/etc/httpd-ssl/private/tls.key"`, servicePort, servicePort) + // Add SSLProtocol only if explicitly set + if TLSVersionToPlugin(r.TLSMinVersion) != "" { + httpdConfigBase += fmt.Sprintf("\n\tSSLProtocol %s", TLSVersionToPlugin(r.TLSMinVersion)) + } + if TLSVersionToPlugin(r.TLSMinVersion) != "TLSv1.3" && strings.Join(r.TLSCiphers, ":") != "" { + httpdConfigBase += fmt.Sprintf("\n\tSSLCipherSuite %s", strings.Join(r.TLSCiphers, ":")) + } + // Close VirtualHost + httpdConfigBase += "\n" + return httpdConfigBase +} + +// pluginConfigMap creates the ConfigMap with dynamic httpd.conf +func (r *ReconcileGitopsService) pluginConfigMap() *corev1.ConfigMap { + httpdConfig := r.buildHttpdConfig() -func pluginConfigMap() *corev1.ConfigMap { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: httpdConfigMapName, @@ -337,7 +367,7 @@ func sortTolerations(tolerations []corev1.Toleration) []corev1.Toleration { return sorted } -func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.GitopsService, request reconcile.Request) (reconcile.Result, error) { +func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.GitopsService, request reconcile.Request, newPluginConfigMap *corev1.ConfigMap) (reconcile.Result, error) { reqLogger := logs.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) newPluginDeployment := pluginDeployment(cr.Spec.ImagePullPolicy) @@ -359,6 +389,13 @@ func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.Gitop newPluginDeployment.Spec.Template.Spec.Tolerations = cr.Spec.Tolerations } + // ADD THIS: Get ConfigMap and add hash to pod template annotations + configMapHash := getConfigMapHash(newPluginConfigMap) + if newPluginDeployment.Spec.Template.ObjectMeta.Annotations == nil { + newPluginDeployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + newPluginDeployment.Spec.Template.ObjectMeta.Annotations["httpd-cfg-hash"] = configMapHash + // Check if this Deployment already exists existingPluginDeployment := &appsv1.Deployment{} @@ -381,6 +418,7 @@ func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.Gitop !equality.Semantic.DeepEqual(existingPluginDeployment.Spec.Replicas, newPluginDeployment.Spec.Replicas) || !equality.Semantic.DeepEqual(existingPluginDeployment.Spec.Selector, newPluginDeployment.Spec.Selector) || !equality.Semantic.DeepEqual(existingSpecTemplate.Labels, newSpecTemplate.Labels) || + !equality.Semantic.DeepEqual(existingSpecTemplate.ObjectMeta.Annotations["httpd-cfg-hash"], newSpecTemplate.ObjectMeta.Annotations["httpd-cfg-hash"]) || !equality.Semantic.DeepEqual(sortContainers(existingSpecTemplate.Spec.Containers), sortContainers(newSpecTemplate.Spec.Containers)) || !equality.Semantic.DeepEqual(sortVolumes(existingSpecTemplate.Spec.Volumes), sortVolumes(newSpecTemplate.Spec.Volumes)) || !equality.Semantic.DeepEqual(existingSpecTemplate.Spec.RestartPolicy, newSpecTemplate.Spec.RestartPolicy) || @@ -396,6 +434,7 @@ func (r *ReconcileGitopsService) reconcileDeployment(cr *pipelinesv1alpha1.Gitop existingPluginDeployment.Spec.Replicas = newPluginDeployment.Spec.Replicas existingPluginDeployment.Spec.Selector = newPluginDeployment.Spec.Selector existingSpecTemplate.Labels = newSpecTemplate.Labels + existingSpecTemplate.ObjectMeta.Annotations["httpd-cfg-hash"] = newSpecTemplate.ObjectMeta.Annotations["httpd-cfg-hash"] existingSpecTemplate.Spec.SecurityContext = newSpecTemplate.Spec.SecurityContext existingSpecTemplate.Spec.Containers = newSpecTemplate.Spec.Containers existingSpecTemplate.Spec.Volumes = newSpecTemplate.Spec.Volumes @@ -487,9 +526,18 @@ func (r *ReconcileGitopsService) reconcileConsolePlugin(instance *pipelinesv1alp return reconcile.Result{}, nil } -func (r *ReconcileGitopsService) reconcileConfigMap(instance *pipelinesv1alpha1.GitopsService, request reconcile.Request) (reconcile.Result, error) { +// getConfigMapHash returns MD5 hash of ConfigMap data for change detection +func getConfigMapHash(cm *corev1.ConfigMap) string { + hash := md5.New() + for key, value := range cm.Data { + io.WriteString(hash, key) + io.WriteString(hash, value) + } + return fmt.Sprintf("%x", hash.Sum(nil)) +} + +func (r *ReconcileGitopsService) reconcileConfigMap(instance *pipelinesv1alpha1.GitopsService, request reconcile.Request, newPluginConfigMap *corev1.ConfigMap) (reconcile.Result, error) { reqLogger := logs.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) - newPluginConfigMap := pluginConfigMap() if err := controllerutil.SetControllerReference(instance, newPluginConfigMap, r.Scheme); err != nil { return reconcile.Result{}, err @@ -529,15 +577,18 @@ func (r *ReconcileGitopsService) reconcilePlugin(instance *pipelinesv1alpha1.Git return reconcile.Result{}, nil } + // Generate ConfigMap once + newPluginConfigMap := r.pluginConfigMap() + if result, err := r.reconcileService(instance, request); err != nil { return result, err } - if result, err := r.reconcileDeployment(instance, request); err != nil { + if result, err := r.reconcileDeployment(instance, request, newPluginConfigMap); err != nil { return result, err } - if result, err := r.reconcileConfigMap(instance, request); err != nil { + if result, err := r.reconcileConfigMap(instance, request, newPluginConfigMap); err != nil { return result, err } diff --git a/controllers/consoleplugin_test.go b/controllers/consoleplugin_test.go index 8624f383d89..1e03a3668df 100644 --- a/controllers/consoleplugin_test.go +++ b/controllers/consoleplugin_test.go @@ -2,6 +2,8 @@ package controllers import ( "context" + "fmt" + "strings" "testing" argocommon "github.com/argoproj-labs/argocd-operator/common" @@ -9,10 +11,12 @@ import ( pipelinesv1alpha1 "github.com/redhat-developer/gitops-operator/api/v1alpha1" "github.com/redhat-developer/gitops-operator/common" "gotest.tools/assert" + "gotest.tools/assert/cmp" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" resourcev1 "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/scheme" @@ -598,7 +602,7 @@ func TestPlugin_reconcileDeployment_changedTemplateLabels(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(newGitopsService(), d).Build() reconciler := newReconcileGitOpsService(fakeClient, s) - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment := &appsv1.Deployment{} @@ -643,7 +647,7 @@ func TestPlugin_reconcileDeployment_changedContainers(t *testing.T) { assert.DeepEqual(t, deployment.Spec.Template.Spec.Containers[0].SecurityContext, securityContextForPlugin()) } - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // There should be a new console plugin deployment created @@ -668,7 +672,7 @@ func TestPlugin_reconcileDeployment_changedContainers(t *testing.T) { assertNoError(t, err) // Verify if the containers are reconciled back to the default values - _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment = &appsv1.Deployment{} @@ -975,7 +979,7 @@ func TestPlugin_reconcileDeployment_infraNodeSelectorNotInPodSpec(t *testing.T) fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(gitopsService).Build() reconciler := newReconcileGitOpsService(fakeClient, s) - _, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment := &appsv1.Deployment{} @@ -995,7 +999,7 @@ func TestPlugin_reconcileDeployment(t *testing.T) { reconciler := newReconcileGitOpsService(fakeClient, s) instance := &pipelinesv1alpha1.GitopsService{} - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment := &appsv1.Deployment{} @@ -1032,7 +1036,7 @@ func TestPlugin_reconcileDeployment_ChangedResources(t *testing.T) { }, } - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment := &appsv1.Deployment{} @@ -1052,7 +1056,7 @@ func TestPlugin_ReconcileDeployment_DefaultResourceValues(t *testing.T) { fakeClient := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(newGitopsService()).Build() reconciler := newReconcileGitOpsService(fakeClient, s) instance := &pipelinesv1alpha1.GitopsService{} - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment := &appsv1.Deployment{} @@ -1092,7 +1096,7 @@ func TestPlugin_ReconcileDeployment_ChangeExistingResourceValues(t *testing.T) { Resources: Resources, }, } - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment := &appsv1.Deployment{} @@ -1116,7 +1120,7 @@ func TestPlugin_ReconcileDeployment_ChangeExistingResourceValues(t *testing.T) { } instance.Spec.ConsolePlugin.Backend.Resources, instance.Spec.ConsolePlugin.GitopsPlugin.Resources = updatedResources, updatedResources - _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) deployment = &appsv1.Deployment{} @@ -1589,7 +1593,7 @@ func TestPlug_reconcileConfigMap(t *testing.T) { reconciler := newReconcileGitOpsService(fakeClient, s) instance := &pipelinesv1alpha1.GitopsService{} - _, err := reconciler.reconcileConfigMap(instance, newRequest(serviceNamespace, httpdConfigMapName)) + _, err := reconciler.reconcileConfigMap(instance, newRequest(serviceNamespace, httpdConfigMapName), reconciler.pluginConfigMap()) assertNoError(t, err) configMap := &corev1.ConfigMap{} @@ -1634,7 +1638,7 @@ func TestPlug_reconcileConfigMap_changedLabels(t *testing.T) { Labels: test.labels, }, Data: map[string]string{ - "httpd.conf": httpdConfig, + "httpd.conf": reconciler.buildHttpdConfig(), }, } if x == 0 { @@ -1665,7 +1669,7 @@ func TestReconcileDeployment_NoUpdateWhenContainersOrderDiffers(t *testing.T) { instance := &pipelinesv1alpha1.GitopsService{} // Create deployment - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Get the deployment and capture initial ResourceVersion and Generation @@ -1688,7 +1692,7 @@ func TestReconcileDeployment_NoUpdateWhenContainersOrderDiffers(t *testing.T) { } // Reconcile again - should NOT trigger an update - _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Verify no update was triggered @@ -1711,7 +1715,7 @@ func TestReconcileDeployment_NoUpdateWhenVolumesOrderDiffers(t *testing.T) { instance := &pipelinesv1alpha1.GitopsService{} // Create deployment - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Get the deployment @@ -1736,7 +1740,7 @@ func TestReconcileDeployment_NoUpdateWhenVolumesOrderDiffers(t *testing.T) { genAfterManualUpdate := deployment.Generation // Reconcile again - should NOT trigger an update - _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Verify no update was triggered @@ -1782,7 +1786,7 @@ func TestReconcileDeployment_NoUpdateWhenTolerationsOrderDiffers(t *testing.T) { reconciler := newReconcileGitOpsService(fakeClient, s) // Create deployment - _, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Get the deployment @@ -1807,7 +1811,7 @@ func TestReconcileDeployment_NoUpdateWhenTolerationsOrderDiffers(t *testing.T) { genAfterManualUpdate := deployment.Generation // Reconcile again - should NOT trigger an update - _, err = reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName)) + _, err = reconciler.reconcileDeployment(gitopsService, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Verify no update was triggered @@ -1833,7 +1837,7 @@ func TestReconcileDeployment_UpdateWhenActualChange(t *testing.T) { instance := &pipelinesv1alpha1.GitopsService{} // Create deployment - _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err := reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Get the deployment and capture initial ResourceVersion and Generation @@ -1848,7 +1852,7 @@ func TestReconcileDeployment_UpdateWhenActualChange(t *testing.T) { assertNoError(t, err) // Reconcile again - should trigger an update - _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName)) + _, err = reconciler.reconcileDeployment(instance, newRequest(serviceNamespace, gitopsPluginName), reconciler.pluginConfigMap()) assertNoError(t, err) // Verify update was triggered @@ -1858,3 +1862,313 @@ func TestReconcileDeployment_UpdateWhenActualChange(t *testing.T) { assert.Assert(t, deployment.ResourceVersion != initialRV, "ResourceVersion SHOULD change when actual change is made") } + +func TestTLSVersionToPlugin(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "TLS 1.2", + input: "VersionTLS12", + expected: "TLSv1.2", + }, + { + name: "TLS 1.3", + input: "VersionTLS13", + expected: "TLSv1.3", + }, + { + name: "TLS 1.1", + input: "VersionTLS11", + expected: "TLSv1.1", + }, + { + name: "TLS 1.0", + input: "VersionTLS10", + expected: "TLSv1", + }, + { + name: "unknown version", + input: "VersionTLS14", + expected: "", + }, + { + name: "empty string", + input: "", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := TLSVersionToPlugin(tt.input) + if actual != tt.expected { + t.Errorf("TLSVersionToPlugin(%q) = %q, want %q", tt.input, actual, tt.expected) + } + }) + } +} + +func TestBuildHttpdConfig(t *testing.T) { + tests := []struct { + name string + minVersion string + ciphers []string + wantProtocol bool + wantCipherSuite bool + protocol string + cipherSuite string + }{ + { + name: "no TLS version no ciphers", + minVersion: "", + ciphers: nil, + wantProtocol: false, + wantCipherSuite: false, + }, + { + name: "TLS 1.2 with cipher suites", + minVersion: "VersionTLS12", + ciphers: []string{"ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-RSA-AES128-GCM-SHA256"}, + wantProtocol: true, + wantCipherSuite: true, + protocol: "TLSv1.2", + cipherSuite: "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256", + }, + { + name: "TLS 1.3 ignores cipher suites", + minVersion: "VersionTLS13", + ciphers: []string{"TLS_AES_128_GCM_SHA256"}, + wantProtocol: true, + wantCipherSuite: false, + protocol: "TLSv1.3", + }, + { + name: "TLS 1.2 without cipher suites", + minVersion: "VersionTLS12", + ciphers: nil, + wantProtocol: true, + wantCipherSuite: false, + protocol: "TLSv1.2", + }, + { + name: "invalid TLS version with cipher suites", + minVersion: "invalid", + ciphers: []string{"ECDHE-RSA-AES256-GCM-SHA384"}, + wantProtocol: false, + wantCipherSuite: true, + cipherSuite: "ECDHE-RSA-AES256-GCM-SHA384", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := &ReconcileGitopsService{ + TLSMinVersion: tt.minVersion, + TLSCiphers: tt.ciphers, + } + + cfg := r.buildHttpdConfig() + + // Base configuration + assert.Assert(t, cmp.Contains(cfg, "LoadModule ssl_module modules/mod_ssl.so")) + assert.Assert(t, cmp.Contains(cfg, fmt.Sprintf("Listen %d https", servicePort))) + assert.Assert(t, cmp.Contains(cfg, fmt.Sprintf("", servicePort))) + assert.Assert(t, cmp.Contains(cfg, `SSLCertificateFile "/etc/httpd-ssl/certs/tls.crt"`)) + assert.Assert(t, cmp.Contains(cfg, `SSLCertificateKeyFile "/etc/httpd-ssl/private/tls.key"`)) + assert.Assert(t, cmp.Contains(cfg, "")) + + // SSLProtocol + if tt.wantProtocol { + assert.Assert(t, cmp.Contains(cfg, "SSLProtocol "+tt.protocol)) + } else { + assert.Assert(t, !strings.Contains(cfg, "SSLProtocol")) + } + + // SSLCipherSuite + if tt.wantCipherSuite { + assert.Assert(t, cmp.Contains(cfg, "SSLCipherSuite "+tt.cipherSuite)) + } else { + assert.Assert(t, !strings.Contains(cfg, "SSLCipherSuite")) + } + }) + } +} + +func TestGetConfigMapHash(t *testing.T) { + t.Run("same configmap returns same hash", func(t *testing.T) { + cm1 := &corev1.ConfigMap{ + Data: map[string]string{ + "config": "value", + "foo": "bar", + }, + } + + cm2 := &corev1.ConfigMap{ + Data: map[string]string{ + "config": "value", + "foo": "bar", + }, + } + + hash1 := getConfigMapHash(cm1) + hash2 := getConfigMapHash(cm2) + + assert.Equal(t, hash1, hash2) + }) + t.Run("different configmaps return different hashes", func(t *testing.T) { + cm1 := &corev1.ConfigMap{ + Data: map[string]string{ + "config": "value1", + }, + } + + cm2 := &corev1.ConfigMap{ + Data: map[string]string{ + "config": "value2", + }, + } + + hash1 := getConfigMapHash(cm1) + hash2 := getConfigMapHash(cm2) + + assert.Assert(t, hash1 != hash2) + }) + t.Run("empty configmap returns deterministic hash", func(t *testing.T) { + cm := &corev1.ConfigMap{ + Data: map[string]string{}, + } + + hash := getConfigMapHash(cm) + + // MD5 of an empty string. + assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", hash) + }) +} + +func TestReconcileDeployment_AddsHashAnnotation(t *testing.T) { + gitopsService := "gitops-service" + serviceNamespace := "openshift-gitops" + scheme := runtime.NewScheme() + assert.NilError(t, appsv1.AddToScheme(scheme)) + assert.NilError(t, corev1.AddToScheme(scheme)) + assert.NilError(t, pipelinesv1alpha1.AddToScheme(scheme)) + + instance := &pipelinesv1alpha1.GitopsService{ + ObjectMeta: metav1.ObjectMeta{ + Name: gitopsService, + Namespace: serviceNamespace, + }, + } + + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "plugin-config", + Namespace: serviceNamespace, + }, + Data: map[string]string{ + "httpd.conf": "config-v1", + }, + } + + r := &ReconcileGitopsService{ + Client: fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(instance). + Build(), + Scheme: scheme, + } + + _, err := r.reconcileDeployment( + instance, + newRequest(serviceNamespace, gitopsService), + cm, + ) + assert.NilError(t, err) + + deployment := &appsv1.Deployment{} + err = r.Client.Get(context.TODO(), + types.NamespacedName{ + Name: gitopsPluginName, + Namespace: serviceNamespace, + }, + deployment, + ) + assert.NilError(t, err) + + expectedHash := getConfigMapHash(cm) + + assert.Equal( + t, + expectedHash, + deployment.Spec.Template.Annotations["httpd-cfg-hash"], + ) +} + +func TestReconcileDeployment_UpdatesHashAnnotationWhenConfigChanges(t *testing.T) { + scheme := runtime.NewScheme() + gitopsService := "gitops-service" + serviceNamespace := "openshift-gitops" + assert.NilError(t, appsv1.AddToScheme(scheme)) + assert.NilError(t, corev1.AddToScheme(scheme)) + assert.NilError(t, pipelinesv1alpha1.AddToScheme(scheme)) + + instance := &pipelinesv1alpha1.GitopsService{ + ObjectMeta: metav1.ObjectMeta{ + Name: gitopsService, + Namespace: serviceNamespace, + }, + } + + cm1 := &corev1.ConfigMap{ + Data: map[string]string{ + "httpd.conf": "config-v1", + }, + } + + cm2 := &corev1.ConfigMap{ + Data: map[string]string{ + "httpd.conf": "config-v2", + }, + } + + r := &ReconcileGitopsService{ + Client: fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(instance). + Build(), + Scheme: scheme, + } + + _, err := r.reconcileDeployment( + instance, + newRequest(serviceNamespace, gitopsService), + cm1, + ) + assert.NilError(t, err) + + _, err = r.reconcileDeployment( + instance, + newRequest(serviceNamespace, gitopsService), + cm2, + ) + assert.NilError(t, err) + + deployment := &appsv1.Deployment{} + err = r.Client.Get(context.TODO(), + types.NamespacedName{ + Name: gitopsPluginName, + Namespace: serviceNamespace, + }, + deployment, + ) + assert.NilError(t, err) + + assert.Equal( + t, + getConfigMapHash(cm2), + deployment.Spec.Template.Annotations["httpd-cfg-hash"], + ) +} diff --git a/controllers/gitopsservice_controller.go b/controllers/gitopsservice_controller.go index b643cad0529..dac21b6c63f 100644 --- a/controllers/gitopsservice_controller.go +++ b/controllers/gitopsservice_controller.go @@ -146,6 +146,10 @@ type ReconcileGitopsService struct { // disableDefaultInstall, if true, will ensure that the default ArgoCD instance is not instantiated in the openshift-gitops namespace. DisableDefaultInstall bool + // TLSMinVersion is the minimum TLS version to use for the GitOps plugin. + TLSMinVersion string + // TLSCiphers is the list of supported TLS ciphers for the GitOps plugin. + TLSCiphers []string } // +kubebuilder:rbac:groups=config.openshift.io,resources=authentications,verbs=get;list;watch