diff --git a/pkg/reconciler/openshift/tektonpruner/extension.go b/pkg/reconciler/openshift/tektonpruner/extension.go index a7b9744c57..f8021fd81a 100644 --- a/pkg/reconciler/openshift/tektonpruner/extension.go +++ b/pkg/reconciler/openshift/tektonpruner/extension.go @@ -21,32 +21,65 @@ import ( mf "github.com/manifestival/manifestival" "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig" "github.com/tektoncd/operator/pkg/reconciler/common" occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common" + "knative.dev/pkg/logging" +) + +const ( + tektonPrunerWebhookDeployment = "tekton-pruner-webhook" + webhookContainerName = "webhook" ) func OpenShiftExtension(ctx context.Context) common.Extension { - return openshiftExtension{} + return &openshiftExtension{ + tektonConfigLister: tektonConfiginformer.Get(ctx).Lister(), + } } -type openshiftExtension struct{} +type openshiftExtension struct { + tektonConfigLister occommon.TektonConfigLister + resolvedTLSConfig *occommon.TLSEnvVars +} -func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer { - return []mf.Transformer{ +func (oe *openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer { + trns := []mf.Transformer{ occommon.RemoveRunAsUser(), occommon.RemoveRunAsGroup(), } + + if oe.resolvedTLSConfig != nil { + trns = append(trns, + occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonPrunerWebhookDeployment, []string{webhookContainerName}, occommon.WebhookEnvVarPrefix), + ) + } + + return trns } -func (oe openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) PreReconcile(ctx context.Context, tc v1alpha1.TektonComponent) error { + logger := logging.FromContext(ctx) + + resolvedTLS, err := occommon.ResolveCentralTLSToEnvVars(ctx, oe.tektonConfigLister) + if err != nil { + return err + } + oe.resolvedTLSConfig = resolvedTLS + if oe.resolvedTLSConfig != nil { + logger.Infof("Injecting central TLS config into pruner webhook: MinVersion=%s", oe.resolvedTLSConfig.MinVersion) + } + return nil } -func (oe openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) PostReconcile(context.Context, v1alpha1.TektonComponent) error { return nil } -func (oe openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error { +func (oe *openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error { return nil } -func (oe openshiftExtension) GetPlatformData() string { +func (oe *openshiftExtension) GetPlatformData() string { return "" } diff --git a/pkg/reconciler/openshift/tektonpruner/extension_test.go b/pkg/reconciler/openshift/tektonpruner/extension_test.go new file mode 100644 index 0000000000..4a2b9518e4 --- /dev/null +++ b/pkg/reconciler/openshift/tektonpruner/extension_test.go @@ -0,0 +1,200 @@ +/* +Copyright 2026 The Tekton Authors + +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 tektonpruner + +import ( + "testing" + + mf "github.com/manifestival/manifestival" + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func makePrunerWebhookDeployment(t *testing.T) unstructured.Unstructured { + t.Helper() + + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: tektonPrunerWebhookDeployment, + Namespace: "openshift-pipelines", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: webhookContainerName}, + }, + }, + }, + }, + } + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d) + if err != nil { + t.Fatalf("failed to convert deployment to unstructured: %v", err) + } + u := unstructured.Unstructured{Object: obj} + u.SetKind("Deployment") + u.SetAPIVersion("apps/v1") + return u +} + +func TestPrunerTransformers_NoTLSConfig(t *testing.T) { + ext := &openshiftExtension{ + resolvedTLSConfig: nil, + } + + transformers := ext.Transformers(&v1alpha1.TektonPruner{}) + + u := makePrunerWebhookDeployment(t) + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + d := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name != webhookContainerName { + continue + } + for _, e := range c.Env { + if e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSMinVersionEnvVar || + e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s set when resolvedTLSConfig is nil", e.Name) + } + } + } +} + +func TestPrunerTransformers_WithTLSConfig_InjectsEnvVarsIntoWebhook(t *testing.T) { + tlsConfig := &occommon.TLSEnvVars{ + MinVersion: "1.2", + CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256", + } + ext := &openshiftExtension{ + resolvedTLSConfig: tlsConfig, + } + + transformers := ext.Transformers(&v1alpha1.TektonPruner{}) + + u := makePrunerWebhookDeployment(t) + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + d := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + + envMap := map[string]string{} + for _, c := range d.Spec.Template.Spec.Containers { + if c.Name != webhookContainerName { + continue + } + for _, e := range c.Env { + envMap[e.Name] = e.Value + } + } + + webhookMinVersion := occommon.WebhookEnvVarPrefix + occommon.TLSMinVersionEnvVar + webhookCipherSuites := occommon.WebhookEnvVarPrefix + occommon.TLSCipherSuitesEnvVar + + if got := envMap[webhookMinVersion]; got != tlsConfig.MinVersion { + t.Errorf("%s = %q, want %q", webhookMinVersion, got, tlsConfig.MinVersion) + } + if got := envMap[webhookCipherSuites]; got != tlsConfig.CipherSuites { + t.Errorf("%s = %q, want %q", webhookCipherSuites, got, tlsConfig.CipherSuites) + } +} + +func TestPrunerTransformers_WithTLSConfig_DoesNotInjectIntoOtherDeployments(t *testing.T) { + tlsConfig := &occommon.TLSEnvVars{ + MinVersion: "1.3", + CipherSuites: "TLS_AES_128_GCM_SHA256", + } + ext := &openshiftExtension{ + resolvedTLSConfig: tlsConfig, + } + + transformers := ext.Transformers(&v1alpha1.TektonPruner{}) + + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tekton-pruner-controller", + Namespace: "openshift-pipelines", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "controller"}, + }, + }, + }, + }, + } + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d) + if err != nil { + t.Fatalf("failed to convert: %v", err) + } + u := unstructured.Unstructured{Object: obj} + u.SetKind("Deployment") + u.SetAPIVersion("apps/v1") + + manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u})) + if err != nil { + t.Fatalf("failed to build manifest: %v", err) + } + + transformed, err := manifest.Transform(transformers...) + if err != nil { + t.Fatalf("transform failed: %v", err) + } + + result := &appsv1.Deployment{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, result); err != nil { + t.Fatalf("failed to convert back: %v", err) + } + + for _, c := range result.Spec.Template.Spec.Containers { + for _, e := range c.Env { + if e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSMinVersionEnvVar || + e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s injected into non-webhook deployment", e.Name) + } + } + } +} diff --git a/pkg/reconciler/shared/tektonconfig/pruner/pruner.go b/pkg/reconciler/shared/tektonconfig/pruner/pruner.go index 608c2629b9..86aa034b5e 100644 --- a/pkg/reconciler/shared/tektonconfig/pruner/pruner.go +++ b/pkg/reconciler/shared/tektonconfig/pruner/pruner.go @@ -128,6 +128,16 @@ func UpdatePruner(ctx context.Context, old *v1alpha1.TektonPruner, new *v1alpha1 updated = true } + oldPlatformData := old.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] + newPlatformData := new.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] + if oldPlatformData != newPlatformData { + if old.ObjectMeta.Annotations == nil { + old.ObjectMeta.Annotations = map[string]string{} + } + old.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] = newPlatformData + updated = true + } + if updated { _, err := clients.Update(ctx, old, metav1.UpdateOptions{}) if err != nil { diff --git a/pkg/reconciler/shared/tektonconfig/tektonconfig.go b/pkg/reconciler/shared/tektonconfig/tektonconfig.go index 84af2599b4..022bc3c0f2 100644 --- a/pkg/reconciler/shared/tektonconfig/tektonconfig.go +++ b/pkg/reconciler/shared/tektonconfig/tektonconfig.go @@ -209,8 +209,14 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfi tc.Status.MarkComponentNotReady(msg) return v1alpha1.REQUEUE_EVENT_AFTER } else { - logger.Infof("TektonPruner is enabled.Creating TektonPipeline CR") + logger.Infof("TektonPruner is enabled. Creating TektonPruner CR") tektonPruner := pruner.GetTektonPrunerCR(tc, r.operatorVersion) + if platformData := r.extension.GetPlatformData(); platformData != "" { + if tektonPruner.Annotations == nil { + tektonPruner.Annotations = map[string]string{} + } + tektonPruner.Annotations[v1alpha1.PlatformDataHashKey] = platformData + } if _, err := pruner.EnsureTektonPrunerExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonPruners(), tektonPruner); err != nil { tc.Status.MarkComponentNotReady(fmt.Sprintf("TektonPruner %s", err.Error())) return v1alpha1.REQUEUE_EVENT_AFTER