Skip to content

Commit 2c8f2cd

Browse files
committed
feat(tls): inject centrally managed TLS config into pipelines-as-code deployment and webhook
Extend the OpenShift TLS centralization pattern (introduced for Tekton Pipelines and Triggers webhooks) to the Pipelines-as-Code webhook. The openshiftpipelinesascode extension now resolves the cluster-wide APIServer TLS security profile in PreReconcile and injects the resulting TLS_MIN_VERSION and TLS_CIPHER_SUITES environment variables into the pipelines-as-code-webhook deployment (pac-webhook container) via the Transformers step, identical to the approach used for tekton-triggers-webhook. PlatformDataHashKey propagation is wired through EnsureOpenShiftPipelinesAsCodeExists / createOPAC / updateOPAC so that any change to the cluster APIServer TLS profile automatically re-reconciles the OpenShiftPipelinesAsCode CR and redeploys the webhook with the updated settings. Resolves: SRVKP-9616 Made-with: Cursor
1 parent a2af438 commit 2c8f2cd

7 files changed

Lines changed: 282 additions & 36 deletions

File tree

pkg/reconciler/kubernetes/tektonconfig/extension.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func (oe kubernetesExtension) PostReconcile(ctx context.Context, comp v1alpha1.T
6161

6262
pacSpec := configInstance.Spec.PipelinesAsCodeForCurrentPlatform()
6363
if pacSpec != nil && pacSpec.Enable != nil && *pacSpec.Enable {
64-
if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, configInstance.Status.Version); err != nil {
64+
if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, configInstance.Status.Version, ""); err != nil {
6565
configInstance.Status.MarkComponentNotReady(fmt.Sprintf("OpenShiftPipelinesAsCode: %s", err.Error()))
6666
return v1alpha1.REQUEUE_EVENT_AFTER
6767
}

pkg/reconciler/kubernetes/tektoninstallerset/client/check.go

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func verifyMeta(resourceKind, isType string, logger *zap.SugaredLogger, set v1al
135135
// Spec Hash Check
136136
logger.Debugf("%v/%v: spec hash check", resourceKind, isType)
137137

138-
expectedHash, err := hash.Compute(specHashInput(comp))
138+
expectedHash, err := hash.Compute(comp.GetSpec())
139139
if err != nil {
140140
return err
141141
}
@@ -149,17 +149,3 @@ func verifyMeta(resourceKind, isType string, logger *zap.SugaredLogger, set v1al
149149

150150
return nil
151151
}
152-
153-
// specHashInput is the canonical input for computing an InstallerSet's spec hash.
154-
// Including PlatformDataHashKey means that operator-injected platform config
155-
// (e.g. OpenShift APIServer TLS profile) causes InstallerSets to be refreshed
156-
// when that config changes, without requiring the component's spec to change.
157-
func specHashInput(comp v1alpha1.TektonComponent) interface{} {
158-
return struct {
159-
Spec interface{}
160-
PlatformData string
161-
}{
162-
Spec: comp.GetSpec(),
163-
PlatformData: comp.GetAnnotations()[v1alpha1.PlatformDataHashKey],
164-
}
165-
}

pkg/reconciler/openshift/openshiftpipelinesascode/extension.go

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import (
2525
mf "github.com/manifestival/manifestival"
2626
"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
2727
operatorclient "github.com/tektoncd/operator/pkg/client/injection/client"
28+
tektonConfiginformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonconfig"
2829
"github.com/tektoncd/operator/pkg/reconciler/common"
2930
"github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client"
31+
occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common"
3032
"go.uber.org/zap"
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -37,7 +39,13 @@ import (
3739
)
3840

3941
const (
40-
openshiftNS = "openshift"
42+
openshiftNS = "openshift"
43+
pacControllerDeployment = "pipelines-as-code-controller"
44+
pacControllerContainerName = "pac-controller"
45+
pacWatcherDeployment = "pipelines-as-code-watcher"
46+
pacWatcherContainerName = "pac-watcher"
47+
pacWebhookDeployment = "pipelines-as-code-webhook"
48+
pacWebhookContainerName = "pac-webhook"
4149
)
4250

4351
func OpenShiftExtension(ctx context.Context) common.Extension {
@@ -68,13 +76,14 @@ func OpenShiftExtension(ctx context.Context) common.Extension {
6876
}
6977

7078
tisClient := operatorclient.Get(ctx).OperatorV1alpha1().TektonInstallerSets()
71-
return openshiftExtension{
79+
return &openshiftExtension{
7280
// component version is used for metrics, passing a dummy
7381
// value through extension not going to affect execution
7482
installerSetClient: client.NewInstallerSetClient(tisClient, operatorVer, "pipelines-as-code-ext", v1alpha1.KindOpenShiftPipelinesAsCode, nil),
7583
pacManifest: &pacManifest,
7684
pipelineRunTemplates: prTemplates,
7785
kubeClientSet: kubeclient.Get(ctx),
86+
tektonConfigLister: tektonConfiginformer.Get(ctx).Lister(),
7887
}
7988
}
8089

@@ -83,17 +92,48 @@ type openshiftExtension struct {
8392
pacManifest *mf.Manifest
8493
pipelineRunTemplates *mf.Manifest
8594
kubeClientSet kubernetes.Interface
95+
tektonConfigLister occommon.TektonConfigLister
96+
resolvedTLSConfig *occommon.TLSEnvVars
8697
}
8798

88-
func (oe openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
89-
return []mf.Transformer{
99+
func (oe *openshiftExtension) Transformers(comp v1alpha1.TektonComponent) []mf.Transformer {
100+
trns := []mf.Transformer{
90101
InjectNamespaceOwnerForPACWebhook(oe.kubeClientSet, comp.GetSpec().GetTargetNamespace()),
91102
}
103+
104+
// Inject APIServer TLS profile env vars into all three PAC deployments so that
105+
// they apply the cluster-wide TLS version and cipher suite policy (PQC readiness).
106+
if oe.resolvedTLSConfig != nil {
107+
trns = append(trns,
108+
// pac-webhook uses sharedmain.WebhookMainWithConfig (Knative webhook framework) which
109+
// calls knativetls.DefaultConfigFromEnv("WEBHOOK_") → reads WEBHOOK_TLS_* env vars.
110+
occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", pacWebhookDeployment, []string{pacWebhookContainerName}, occommon.WebhookEnvVarPrefix),
111+
// pac-controller and pac-watcher do not serve a TLS endpoint that reads these env vars;
112+
// injecting with no prefix is harmless and keeps them consistent for future use.
113+
occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", pacControllerDeployment, []string{pacControllerContainerName}, ""),
114+
occommon.InjectTLSEnvVars(oe.resolvedTLSConfig, "Deployment", pacWatcherDeployment, []string{pacWatcherContainerName}, ""),
115+
)
116+
}
117+
118+
return trns
92119
}
93-
func (oe openshiftExtension) PreReconcile(context.Context, v1alpha1.TektonComponent) error {
120+
121+
func (oe *openshiftExtension) PreReconcile(ctx context.Context, _ v1alpha1.TektonComponent) error {
122+
logger := logging.FromContext(ctx)
123+
124+
resolvedTLS, err := occommon.ResolveCentralTLSToEnvVars(ctx, oe.tektonConfigLister)
125+
if err != nil {
126+
return err
127+
}
128+
oe.resolvedTLSConfig = resolvedTLS
129+
if oe.resolvedTLSConfig != nil {
130+
logger.Infof("Injecting central TLS config into PAC deployments: MinVersion=%s", oe.resolvedTLSConfig.MinVersion)
131+
}
132+
94133
return nil
95134
}
96-
func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
135+
136+
func (oe *openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.TektonComponent) error {
97137
logger := logging.FromContext(ctx)
98138

99139
if err := oe.installerSetClient.PostSet(ctx, comp, oe.pipelineRunTemplates, extFilterAndTransform()); err != nil {
@@ -107,11 +147,12 @@ func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.Te
107147
}
108148
return nil
109149
}
110-
func (oe openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
150+
151+
func (oe *openshiftExtension) Finalize(context.Context, v1alpha1.TektonComponent) error {
111152
return nil
112153
}
113154

114-
func (oe openshiftExtension) GetPlatformData() string {
155+
func (oe *openshiftExtension) GetPlatformData() string {
115156
return ""
116157
}
117158

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/*
2+
Copyright 2026 The Tekton Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package openshiftpipelinesascode
18+
19+
import (
20+
"testing"
21+
22+
mf "github.com/manifestival/manifestival"
23+
"github.com/tektoncd/operator/pkg/apis/operator/v1alpha1"
24+
occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common"
25+
appsv1 "k8s.io/api/apps/v1"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
)
31+
32+
// makePACDeployment returns an unstructured PAC Deployment for transformer tests.
33+
func makePACDeployment(t *testing.T, deploymentName, containerName string) unstructured.Unstructured {
34+
t.Helper()
35+
36+
d := &appsv1.Deployment{
37+
ObjectMeta: metav1.ObjectMeta{
38+
Name: deploymentName,
39+
Namespace: "openshift-pipelines",
40+
},
41+
Spec: appsv1.DeploymentSpec{
42+
Template: corev1.PodTemplateSpec{
43+
Spec: corev1.PodSpec{
44+
Containers: []corev1.Container{
45+
{Name: containerName},
46+
},
47+
},
48+
},
49+
},
50+
}
51+
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(d)
52+
if err != nil {
53+
t.Fatalf("failed to convert deployment to unstructured: %v", err)
54+
}
55+
u := unstructured.Unstructured{Object: obj}
56+
u.SetKind("Deployment")
57+
u.SetAPIVersion("apps/v1")
58+
return u
59+
}
60+
61+
func makePACWebhookDeployment(t *testing.T) unstructured.Unstructured {
62+
t.Helper()
63+
return makePACDeployment(t, pacWebhookDeployment, pacWebhookContainerName)
64+
}
65+
66+
func TestPACTransformers_NoTLSConfig(t *testing.T) {
67+
ext := &openshiftExtension{
68+
resolvedTLSConfig: nil,
69+
}
70+
71+
transformers := ext.Transformers(&v1alpha1.OpenShiftPipelinesAsCode{})
72+
73+
u := makePACWebhookDeployment(t)
74+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
75+
if err != nil {
76+
t.Fatalf("failed to build manifest: %v", err)
77+
}
78+
79+
transformed, err := manifest.Transform(transformers...)
80+
if err != nil {
81+
t.Fatalf("transform failed: %v", err)
82+
}
83+
84+
d := &appsv1.Deployment{}
85+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil {
86+
t.Fatalf("failed to convert back: %v", err)
87+
}
88+
for _, c := range d.Spec.Template.Spec.Containers {
89+
if c.Name != pacWebhookContainerName {
90+
continue
91+
}
92+
for _, e := range c.Env {
93+
if e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSMinVersionEnvVar ||
94+
e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSCipherSuitesEnvVar {
95+
t.Errorf("unexpected TLS env var %s set when resolvedTLSConfig is nil", e.Name)
96+
}
97+
}
98+
}
99+
}
100+
101+
func TestPACTransformers_WithTLSConfig_InjectsEnvVarsIntoWebhook(t *testing.T) {
102+
tlsConfig := &occommon.TLSEnvVars{
103+
MinVersion: "1.2",
104+
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256",
105+
}
106+
// pac-webhook uses the Knative webhook framework → reads WEBHOOK_TLS_* env vars.
107+
assertTLSInjected(t, &openshiftExtension{resolvedTLSConfig: tlsConfig}, pacWebhookDeployment, pacWebhookContainerName, occommon.WebhookEnvVarPrefix, tlsConfig)
108+
}
109+
110+
// assertTLSInjected runs the Transformers against a single-resource manifest and
111+
// checks that TLS env vars are present in the named container.
112+
// envVarPrefix is prepended to the standard env var names (e.g. occommon.WebhookEnvVarPrefix
113+
// for deployments using the Knative webhook framework, "" for everything else).
114+
func assertTLSInjected(t *testing.T, ext *openshiftExtension, deploymentName, containerName, envVarPrefix string, tlsConfig *occommon.TLSEnvVars) {
115+
t.Helper()
116+
117+
u := makePACDeployment(t, deploymentName, containerName)
118+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
119+
if err != nil {
120+
t.Fatalf("failed to build manifest: %v", err)
121+
}
122+
123+
transformed, err := manifest.Transform(ext.Transformers(&v1alpha1.OpenShiftPipelinesAsCode{})...)
124+
if err != nil {
125+
t.Fatalf("transform failed: %v", err)
126+
}
127+
128+
d := &appsv1.Deployment{}
129+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, d); err != nil {
130+
t.Fatalf("failed to convert back: %v", err)
131+
}
132+
133+
envMap := map[string]string{}
134+
for _, c := range d.Spec.Template.Spec.Containers {
135+
if c.Name != containerName {
136+
continue
137+
}
138+
for _, e := range c.Env {
139+
envMap[e.Name] = e.Value
140+
}
141+
}
142+
143+
minVersionKey := envVarPrefix + occommon.TLSMinVersionEnvVar
144+
cipherSuitesKey := envVarPrefix + occommon.TLSCipherSuitesEnvVar
145+
146+
if got := envMap[minVersionKey]; got != tlsConfig.MinVersion {
147+
t.Errorf("[%s/%s] %s = %q, want %q", deploymentName, containerName, minVersionKey, got, tlsConfig.MinVersion)
148+
}
149+
if got := envMap[cipherSuitesKey]; got != tlsConfig.CipherSuites {
150+
t.Errorf("[%s/%s] %s = %q, want %q", deploymentName, containerName, cipherSuitesKey, got, tlsConfig.CipherSuites)
151+
}
152+
}
153+
154+
func TestPACTransformers_WithTLSConfig_InjectsEnvVarsIntoController(t *testing.T) {
155+
tlsConfig := &occommon.TLSEnvVars{
156+
MinVersion: "1.2",
157+
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256",
158+
}
159+
// pac-controller uses the Knative eventing adapter — no TLS version env var support; no prefix.
160+
assertTLSInjected(t, &openshiftExtension{resolvedTLSConfig: tlsConfig}, pacControllerDeployment, pacControllerContainerName, "", tlsConfig)
161+
}
162+
163+
func TestPACTransformers_WithTLSConfig_InjectsEnvVarsIntoWatcher(t *testing.T) {
164+
tlsConfig := &occommon.TLSEnvVars{
165+
MinVersion: "1.2",
166+
CipherSuites: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_AES_128_GCM_SHA256",
167+
}
168+
// pac-watcher is a plain Knative controller — no TLS server; no prefix.
169+
assertTLSInjected(t, &openshiftExtension{resolvedTLSConfig: tlsConfig}, pacWatcherDeployment, pacWatcherContainerName, "", tlsConfig)
170+
}
171+
172+
func TestPACTransformers_WithTLSConfig_DoesNotInjectIntoUnknownDeployment(t *testing.T) {
173+
tlsConfig := &occommon.TLSEnvVars{
174+
MinVersion: "1.3",
175+
CipherSuites: "TLS_AES_128_GCM_SHA256",
176+
}
177+
ext := &openshiftExtension{resolvedTLSConfig: tlsConfig}
178+
transformers := ext.Transformers(&v1alpha1.OpenShiftPipelinesAsCode{})
179+
180+
// An unrelated deployment must not receive TLS env vars.
181+
u := makePACDeployment(t, "some-other-deployment", "some-container")
182+
manifest, err := mf.ManifestFrom(mf.Slice([]unstructured.Unstructured{u}))
183+
if err != nil {
184+
t.Fatalf("failed to build manifest: %v", err)
185+
}
186+
transformed, err := manifest.Transform(transformers...)
187+
if err != nil {
188+
t.Fatalf("transform failed: %v", err)
189+
}
190+
191+
result := &appsv1.Deployment{}
192+
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(transformed.Resources()[0].Object, result); err != nil {
193+
t.Fatalf("failed to convert back: %v", err)
194+
}
195+
for _, c := range result.Spec.Template.Spec.Containers {
196+
for _, e := range c.Env {
197+
if e.Name == occommon.TLSMinVersionEnvVar || e.Name == occommon.TLSCipherSuitesEnvVar ||
198+
e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSMinVersionEnvVar ||
199+
e.Name == occommon.WebhookEnvVarPrefix+occommon.TLSCipherSuitesEnvVar {
200+
t.Errorf("unexpected TLS env var %s injected into unrelated deployment", e.Name)
201+
}
202+
}
203+
}
204+
}

pkg/reconciler/openshift/tektonconfig/extension.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ func (oe openshiftExtension) PostReconcile(ctx context.Context, comp v1alpha1.Te
191191

192192
pacSpec := configInstance.Spec.PipelinesAsCodeForCurrentPlatform()
193193
if pacSpec != nil && pacSpec.Enable != nil && *pacSpec.Enable {
194-
if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, oe.operatorVersion); err != nil {
194+
if _, err := pac.EnsureOpenShiftPipelinesAsCodeExists(ctx, oe.operatorClientSet.OperatorV1alpha1().OpenShiftPipelinesAsCodes(), configInstance, oe.operatorVersion, oe.GetPlatformData()); err != nil {
195195
configInstance.Status.MarkComponentNotReady(fmt.Sprintf("OpenShiftPipelinesAsCode: %s", err.Error()))
196196
return v1alpha1.REQUEUE_EVENT_AFTER
197197
}

0 commit comments

Comments
 (0)