diff --git a/pkg/reconciler/openshift/tektontrigger/extension.go b/pkg/reconciler/openshift/tektontrigger/extension.go index 5903c162a7..1b53d36c49 100644 --- a/pkg/reconciler/openshift/tektontrigger/extension.go +++ b/pkg/reconciler/openshift/tektontrigger/extension.go @@ -21,12 +21,19 @@ 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" "github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektontrigger" occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common" + "knative.dev/pkg/logging" "knative.dev/pkg/ptr" ) +const ( + tektonTriggersWebhookDeployment = "tekton-triggers-webhook" + webhookContainerName = "webhook" +) + // triggersProperties holds fields for configuring runAsUser and runAsGroup. type triggersProperties struct { DefaultRunAsUser *string `json:"default-run-as-user,omitempty"` @@ -44,30 +51,57 @@ var triggersData = triggersProperties{ } 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(), occommon.ApplyCABundlesToDeployment, common.AddConfigMapValues(tektontrigger.ConfigDefaults, triggersData), replaceDeploymentArgs("-el-events", "enable"), } + + // Inject APIServer TLS profile env vars into the webhook so that it applies + // the cluster-wide TLS version and cipher suite policy (PQC readiness). + if oe.resolvedTLSConfig != nil { + trns = append(trns, occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", tektonTriggersWebhookDeployment, []string{webhookContainerName})) + } + + 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 triggers 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/tektontrigger/extension_test.go b/pkg/reconciler/openshift/tektontrigger/extension_test.go new file mode 100644 index 0000000000..6547693a80 --- /dev/null +++ b/pkg/reconciler/openshift/tektontrigger/extension_test.go @@ -0,0 +1,197 @@ +/* +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 tektontrigger + +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" +) + +// makeTriggersWebhookDeployment returns an unstructured triggers webhook Deployment for transformer tests. +func makeTriggersWebhookDeployment(t *testing.T) unstructured.Unstructured { + t.Helper() + + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: tektonTriggersWebhookDeployment, + 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 TestTriggersTransformers_NoTLSConfig(t *testing.T) { + ext := &openshiftExtension{ + resolvedTLSConfig: nil, + } + + transformers := ext.Transformers(&v1alpha1.TektonTrigger{}) + + u := makeTriggersWebhookDeployment(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.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s set when resolvedTLSConfig is nil", e.Name) + } + } + } +} + +func TestTriggersTransformers_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.TektonTrigger{}) + + u := makeTriggersWebhookDeployment(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 + } + } + + if got := envMap[occommon.TLSMinVersionEnvVar]; got != tlsConfig.MinVersion { + t.Errorf("%s = %q, want %q", occommon.TLSMinVersionEnvVar, got, tlsConfig.MinVersion) + } + if got := envMap[occommon.TLSCipherSuitesEnvVar]; got != tlsConfig.CipherSuites { + t.Errorf("%s = %q, want %q", occommon.TLSCipherSuitesEnvVar, got, tlsConfig.CipherSuites) + } +} + +func TestTriggersTransformers_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.TektonTrigger{}) + + // Use a different deployment name — TLS env vars must NOT be injected. + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tekton-triggers-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.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar { + t.Errorf("unexpected TLS env var %s injected into non-webhook deployment", e.Name) + } + } + } +} diff --git a/pkg/reconciler/shared/tektonconfig/tektonconfig.go b/pkg/reconciler/shared/tektonconfig/tektonconfig.go index 4189669c99..b9550856c1 100644 --- a/pkg/reconciler/shared/tektonconfig/tektonconfig.go +++ b/pkg/reconciler/shared/tektonconfig/tektonconfig.go @@ -260,6 +260,12 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tc *v1alpha1.TektonConfi // Ensure Pipeline Trigger if !tc.Spec.Trigger.Disabled && (tc.Spec.Profile == v1alpha1.ProfileAll || tc.Spec.Profile == v1alpha1.ProfileBasic) { tektontrigger := trigger.GetTektonTriggerCR(tc, r.operatorVersion) + if platformData := r.extension.GetPlatformData(); platformData != "" { + if tektontrigger.Annotations == nil { + tektontrigger.Annotations = map[string]string{} + } + tektontrigger.Annotations[v1alpha1.PlatformDataHashKey] = platformData + } logger.Debug("Ensuring TektonTrigger CR exists") if _, err := trigger.EnsureTektonTriggerExists(ctx, r.operatorClientSet.OperatorV1alpha1().TektonTriggers(), tektontrigger); err != nil { errMsg := fmt.Sprintf("TektonTrigger: %s", err.Error()) diff --git a/pkg/reconciler/shared/tektonconfig/trigger/trigger.go b/pkg/reconciler/shared/tektonconfig/trigger/trigger.go index 252fe58865..01525ddb1a 100644 --- a/pkg/reconciler/shared/tektonconfig/trigger/trigger.go +++ b/pkg/reconciler/shared/tektonconfig/trigger/trigger.go @@ -127,6 +127,16 @@ func UpdateTrigger(ctx context.Context, old *v1alpha1.TektonTrigger, new *v1alph 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 {