diff --git a/pkg/reconciler/kubernetes/tektonconfig/extension.go b/pkg/reconciler/kubernetes/tektonconfig/extension.go index 1d26194efa..45fe981230 100644 --- a/pkg/reconciler/kubernetes/tektonconfig/extension.go +++ b/pkg/reconciler/kubernetes/tektonconfig/extension.go @@ -61,7 +61,7 @@ func (oe kubernetesExtension) PostReconcile(ctx context.Context, comp v1alpha1.T pacSpec := configInstance.Spec.PipelinesAsCodeForCurrentPlatform() if pacSpec != nil && pacSpec.Enable != nil && *pacSpec.Enable { - if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, configInstance.Status.Version); err != nil { + if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, configInstance.Status.Version, ""); err != nil { configInstance.Status.MarkComponentNotReady(fmt.Sprintf("OpenShiftPipelinesAsCode: %s", err.Error())) return v1alpha1.REQUEUE_EVENT_AFTER } diff --git a/pkg/reconciler/openshift/openshiftpipelinesascode/extension.go b/pkg/reconciler/openshift/openshiftpipelinesascode/extension.go index 8875e06466..23bb90e8e0 100644 --- a/pkg/reconciler/openshift/openshiftpipelinesascode/extension.go +++ b/pkg/reconciler/openshift/openshiftpipelinesascode/extension.go @@ -25,8 +25,10 @@ import ( mf "github.com/manifestival/manifestival" "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" operatorclient "github.com/tektoncd/operator/pkg/client/injection/client" + 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/tektoninstallerset/client" + occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common" "go.uber.org/zap" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -37,7 +39,9 @@ import ( ) const ( - openshiftNS = "openshift" + openshiftNS = "openshift" + pacWebhookDeployment = "pipelines-as-code-webhook" + pacWebhookContainerName = "pac-webhook" ) func OpenShiftExtension(ctx context.Context) common.Extension { @@ -68,13 +72,14 @@ func OpenShiftExtension(ctx context.Context) common.Extension { } tisClient := operatorclient.Get(ctx).OperatorV1alpha1().TektonInstallerSets() - return openshiftExtension{ + return &openshiftExtension{ // component version is used for metrics, passing a dummy // value through extension not going to affect execution installerSetClient: client.NewInstallerSetClient(tisClient, operatorVer, "pipelines-as-code-ext", v1alpha1.KindOpenShiftPipelinesAsCode, nil), pacManifest: &pacManifest, pipelineRunTemplates: prTemplates, kubeClientSet: kubeclient.Get(ctx), + tektonConfigLister: tektonConfiginformer.Get(ctx).Lister(), } } @@ -83,17 +88,40 @@ type openshiftExtension struct { pacManifest *mf.Manifest pipelineRunTemplates *mf.Manifest kubeClientSet kubernetes.Interface + 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{ InjectNamespaceOwnerForPACWebhook(oe.kubeClientSet, comp.GetSpec().GetTargetNamespace()), } + + // Inject APIServer TLS profile env vars into the PAC 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", pacWebhookDeployment, []string{pacWebhookContainerName})) + } + + return trns } -func (oe openshiftExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) PreReconcile(ctx context.Context, _ 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 PAC webhook: MinVersion=%s", oe.resolvedTLSConfig.MinVersion) + } + return nil } -func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error { + +func (oe *openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error { logger := logging.FromContext(ctx) if err := oe.installerSetClient.PostSet(ctx, comp, oe.pipelineRunTemplates, extFilterAndTransform()); err != nil { @@ -107,11 +135,12 @@ func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.Te } 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/openshiftpipelinesascode/extension_test.go b/pkg/reconciler/openshift/openshiftpipelinesascode/extension_test.go new file mode 100644 index 0000000000..50e3a24cf7 --- /dev/null +++ b/pkg/reconciler/openshift/openshiftpipelinesascode/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 openshiftpipelinesascode + +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" +) + +// makePACWebhookDeployment returns an unstructured PAC webhook Deployment for transformer tests. +func makePACWebhookDeployment(t *testing.T) unstructured.Unstructured { + t.Helper() + + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: pacWebhookDeployment, + Namespace: "openshift-pipelines", + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: pacWebhookContainerName}, + }, + }, + }, + }, + } + 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 TestPACTransformers_NoTLSConfig(t *testing.T) { + ext := &openshiftExtension{ + resolvedTLSConfig: nil, + } + + transformers := ext.Transformers(&v1alpha1.OpenShiftPipelinesAsCode{}) + + u := makePACWebhookDeployment(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 != pacWebhookContainerName { + 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 TestPACTransformers_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.OpenShiftPipelinesAsCode{}) + + u := makePACWebhookDeployment(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 != pacWebhookContainerName { + 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 TestPACTransformers_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.OpenShiftPipelinesAsCode{}) + + // Use a different deployment name — TLS env vars must NOT be injected. + d := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipelines-as-code-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/openshift/tektonconfig/extension.go b/pkg/reconciler/openshift/tektonconfig/extension.go index 8d848f9fc7..43f2face98 100644 --- a/pkg/reconciler/openshift/tektonconfig/extension.go +++ b/pkg/reconciler/openshift/tektonconfig/extension.go @@ -191,7 +191,7 @@ func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.Te pacSpec := configInstance.Spec.PipelinesAsCodeForCurrentPlatform() if pacSpec != nil && pacSpec.Enable != nil && *pacSpec.Enable { - if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, oe.operatorVersion); err != nil { + if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, oe.operatorVersion, oe.GetPlatformData()); err != nil { configInstance.Status.MarkComponentNotReady(fmt.Sprintf("OpenShiftPipelinesAsCode: %s", err.Error())) return v1alpha1.REQUEUE_EVENT_AFTER } diff --git a/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode.go b/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode.go index e088b0ff74..4400ef017b 100644 --- a/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode.go +++ b/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode.go @@ -40,19 +40,19 @@ func pacSettingsFromTektonPAC(p *v1alpha1.PipelinesAsCode) v1alpha1.PACSettings } } -func EnsureOpenShiftPipelinesAsCodeExists(ctx context.Context, clients op.OpenShiftPipelinesAsCodeInterface, config *v1alpha1.TektonConfig, operatorVersion string) (*v1alpha1.OpenShiftPipelinesAsCode, error) { +func EnsureOpenShiftPipelinesAsCodeExists(ctx context.Context, clients op.OpenShiftPipelinesAsCodeInterface, config *v1alpha1.TektonConfig, operatorVersion string, platformData string) (*v1alpha1.OpenShiftPipelinesAsCode, error) { opacCR, err := GetPAC(ctx, clients, v1alpha1.OpenShiftPipelinesAsCodeName) if err != nil { if !apierrs.IsNotFound(err) { return nil, err } - if _, err = createOPAC(ctx, clients, config, operatorVersion); err != nil { + if _, err = createOPAC(ctx, clients, config, operatorVersion, platformData); err != nil { return nil, err } return nil, v1alpha1.RECONCILE_AGAIN_ERR } - opacCR, err = updateOPAC(ctx, opacCR, config, clients, operatorVersion) + opacCR, err = updateOPAC(ctx, opacCR, config, clients, operatorVersion, platformData) if err != nil { return nil, err } @@ -68,11 +68,16 @@ func EnsureOpenShiftPipelinesAsCodeExists(ctx context.Context, clients op.OpenSh return opacCR, err } -func createOPAC(ctx context.Context, clients op.OpenShiftPipelinesAsCodeInterface, config *v1alpha1.TektonConfig, operatorVersion string) (*v1alpha1.OpenShiftPipelinesAsCode, error) { +func createOPAC(ctx context.Context, clients op.OpenShiftPipelinesAsCodeInterface, config *v1alpha1.TektonConfig, operatorVersion string, platformData string) (*v1alpha1.OpenShiftPipelinesAsCode, error) { ownerRef := *metav1.NewControllerRef(config, config.GroupVersionKind()) pacSettings := pacSettingsFromTektonPAC(config.Spec.PipelinesAsCodeForCurrentPlatform()) + annotations := map[string]string{} + if platformData != "" { + annotations[v1alpha1.PlatformDataHashKey] = platformData + } + opacCR := &v1alpha1.OpenShiftPipelinesAsCode{ ObjectMeta: metav1.ObjectMeta{ Name: v1alpha1.OpenShiftPipelinesAsCodeName, @@ -80,6 +85,7 @@ func createOPAC(ctx context.Context, clients op.OpenShiftPipelinesAsCodeInterfac Labels: map[string]string{ v1alpha1.ReleaseVersionKey: operatorVersion, }, + Annotations: annotations, }, Spec: v1alpha1.OpenShiftPipelinesAsCodeSpec{ CommonSpec: v1alpha1.CommonSpec{ @@ -101,7 +107,7 @@ func GetPAC(ctx context.Context, clients op.OpenShiftPipelinesAsCodeInterface, n } func updateOPAC(ctx context.Context, opacCR *v1alpha1.OpenShiftPipelinesAsCode, config *v1alpha1.TektonConfig, - clients op.OpenShiftPipelinesAsCodeInterface, operatorVersion string, + clients op.OpenShiftPipelinesAsCodeInterface, operatorVersion string, platformData string, ) (*v1alpha1.OpenShiftPipelinesAsCode, error) { updated := false @@ -148,6 +154,15 @@ func updateOPAC(ctx context.Context, opacCR *v1alpha1.OpenShiftPipelinesAsCode, updated = true } + oldPlatformData := opacCR.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] + if oldPlatformData != platformData { + if opacCR.ObjectMeta.Annotations == nil { + opacCR.ObjectMeta.Annotations = map[string]string{} + } + opacCR.ObjectMeta.Annotations[v1alpha1.PlatformDataHashKey] = platformData + updated = true + } + if updated { _, err := clients.Update(ctx, opacCR, metav1.UpdateOptions{}) if err != nil { diff --git a/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode_test.go b/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode_test.go index 5343056a4e..edd569eea9 100644 --- a/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode_test.go +++ b/pkg/reconciler/shared/tektonconfig/pipelinesascode/pipelinesascode_test.go @@ -38,22 +38,22 @@ func TestEnsureOpenShiftPipelinesAsCodeExists(t *testing.T) { t.Setenv("PLATFORM", "openshift") tConfig.SetDefaults(ctx) - _, err := EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0") + _, err := EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0", "") util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) - _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0") + _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0", "") util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) markOPACReady(t, ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes()) - _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0") + _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0", "") util.AssertEqual(t, err, nil) tConfig.Spec.TargetNamespace = "foobar" - _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0") + _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0", "") util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) - _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0") + _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0", "") util.AssertEqual(t, err, nil) } @@ -68,7 +68,7 @@ func TestEnsureOpenShiftPipelinesAsCodeCRNotExists(t *testing.T) { tConfig := pipeline.GetTektonConfig() tConfig.SetDefaults(ctx) - _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0") + _, err = EnsureOpenShiftPipelinesAsCodeExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), tConfig, "v0.70.0", "") util.AssertEqual(t, err, v1alpha1.RECONCILE_AGAIN_ERR) err = EnsureOpenShiftPipelinesAsCodeCRNotExists(ctx, c.OperatorV1alpha1().OpenShiftPipelinesAsCodes())