Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions cmd/kubernetes/proxy-webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,39 @@ limitations under the License.
package main

import (
"os"

"github.com/tektoncd/operator/pkg/reconciler/proxy"
"knative.dev/pkg/injection"
"knative.dev/pkg/injection/sharedmain"
"knative.dev/pkg/signals"
kwebhook "knative.dev/pkg/webhook"
"knative.dev/pkg/webhook/certificates"
)

func main() {
sharedmain.WebhookMainWithConfig(proxy.Getctx(), "webhook-operator",
injection.ParseAndGetRESTConfigOrDie(),
serviceName := os.Getenv("WEBHOOK_SERVICE_NAME")
if serviceName == "" {
serviceName = "tekton-operator-proxy-webhook"
}
secretName := os.Getenv("WEBHOOK_SECRET_NAME")
if secretName == "" {
secretName = "proxy-webhook-certs"
}
systemNamespace := os.Getenv("SYSTEM_NAMESPACE")

cfg := injection.ParseAndGetRESTConfigOrDie()
ctx := kwebhook.WithOptions(
injection.WithNamespaceScope(signals.NewContext(), systemNamespace),
kwebhook.Options{
ServiceName: serviceName,
Port: 8443,
SecretName: secretName,
},
)

sharedmain.WebhookMainWithConfig(ctx, "webhook-operator",
cfg,
certificates.NewController,
proxy.NewProxyDefaultingAdmissionController,
)
Expand Down
4 changes: 4 additions & 0 deletions cmd/openshift/operator/kodata/webhook/webhook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ rules:
- apiGroups: ["security.openshift.io"]
resources: ["securitycontextconstraints"]
verbs: ["get", "list", "watch"]
# Required to read the OpenShift APIServer TLS profile at startup
- apiGroups: ["config.openshift.io"]
resources: ["apiservers"]
verbs: ["get", "list", "watch"]

---

Expand Down
65 changes: 63 additions & 2 deletions cmd/openshift/proxy-webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ package main

import (
"context"
"log"
"os"

"github.com/tektoncd/operator/pkg/reconciler/openshift/annotation"
occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common"
"github.com/tektoncd/operator/pkg/reconciler/openshift/namespace"
"github.com/tektoncd/operator/pkg/reconciler/proxy"
"knative.dev/pkg/configmap"
"knative.dev/pkg/controller"
"knative.dev/pkg/injection"
"knative.dev/pkg/injection/sharedmain"
"knative.dev/pkg/signals"
kwebhook "knative.dev/pkg/webhook"
"knative.dev/pkg/webhook/certificates"
)

Expand All @@ -50,8 +55,64 @@ func newAnnotationDefaultingAdmissionController(ctx context.Context, cmw configm
}

func main() {
sharedmain.WebhookMainWithConfig(proxy.Getctx(), "webhook-operator",
injection.ParseAndGetRESTConfigOrDie(),
serviceName := os.Getenv("WEBHOOK_SERVICE_NAME")
if serviceName == "" {
serviceName = "tekton-operator-proxy-webhook"
}
secretName := os.Getenv("WEBHOOK_SECRET_NAME")
if secretName == "" {
secretName = "proxy-webhook-certs"
}

cfg := injection.ParseAndGetRESTConfigOrDie()
signalCtx := signals.NewContext()

if err := occommon.SetupAPIServerTLSWatch(signalCtx, cfg, func() {
log.Println("APIServer TLS profile changed — restarting proxy webhook to apply updated settings")
os.Exit(1)
}); err != nil {
if os.Getenv(occommon.SkipAPIServerTLSWatch) == "true" {
log.Printf("WARNING: APIServer TLS watch not available, using Knative defaults: %v", err)
} else {
log.Fatalf("Failed to set up APIServer TLS watch: %v", err)
}
}

if tlsProfile, err := occommon.GetTLSProfileFromAPIServer(signalCtx); err != nil {
log.Printf("WARNING: could not read APIServer TLS profile, using Knative defaults: %v", err)
} else if tlsProfile != nil {
if envVars, err := occommon.TLSEnvVarsFromProfile(tlsProfile); err != nil {
log.Printf("WARNING: could not convert TLS profile, using Knative defaults: %v", err)
} else if envVars != nil {
// Knative only accepts "1.2" or "1.3"; skip if the profile allows older versions
// (e.g. OpenShift "Old" profile uses VersionTLS10). The webhook will then fall
// back to Knative's default minimum of 1.2, which is always safe for admission webhooks.
if envVars.MinVersion == "1.2" || envVars.MinVersion == "1.3" {
os.Setenv("WEBHOOK_TLS_MIN_VERSION", envVars.MinVersion)
}
if envVars.CipherSuites != "" {
os.Setenv("WEBHOOK_TLS_CIPHER_SUITES", envVars.CipherSuites)
}
if envVars.CurvePreferences != "" {
os.Setenv("WEBHOOK_TLS_CURVE_PREFERENCES", envVars.CurvePreferences)
}
}
}

// Inline the context setup (same as proxy.Getctx but reuses the signal
// context already created above instead of calling signals.NewContext again).
systemNamespace := os.Getenv("SYSTEM_NAMESPACE")
ctx := kwebhook.WithOptions(
injection.WithNamespaceScope(signalCtx, systemNamespace),
kwebhook.Options{
ServiceName: serviceName,
Port: 8443,
SecretName: secretName,
},
)

sharedmain.WebhookMainWithConfig(ctx, "webhook-operator",
cfg,
certificates.NewController,
proxy.NewProxyDefaultingAdmissionController,
newAnnotationDefaultingAdmissionController,
Expand Down
55 changes: 52 additions & 3 deletions cmd/openshift/webhook/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package main

import (
"context"
"log"
"os"

occommon "github.com/tektoncd/operator/pkg/reconciler/openshift/common"
"github.com/tektoncd/operator/pkg/webhook"
"knative.dev/pkg/injection"
"knative.dev/pkg/injection/sharedmain"
Expand All @@ -39,13 +41,60 @@ func main() {
secretName = "tekton-operator-webhook-certs"
}

//Set up a signal context with our webhook options
ctx := kwebhook.WithOptions(signals.NewContext(), kwebhook.Options{
// cfg is obtained before signals.NewContext so it's available for the TLS watch setup.
cfg := injection.ParseAndGetRESTConfigOrDie()
signalCtx := signals.NewContext()

// Set up the APIServer TLS watch using the same helper as the TektonConfig controller.
// - Populates the shared lister so GetTLSProfileFromAPIServer works below.
// - Calls os.Exit(1) when the TLS profile changes; Kubernetes restartPolicy: Always
// restarts the container so the new instance picks up the updated profile.
if err := occommon.SetupAPIServerTLSWatch(signalCtx, cfg, func() {
log.Println("APIServer TLS profile changed — restarting webhook to apply updated settings")
os.Exit(1)
}); err != nil {
// On OpenShift clusters the APIServer resource should always exist.
// SKIP_APISERVER_TLS_WATCH=true is an escape hatch for tests/edge cases —
// same pattern as the TektonConfig controller.
if os.Getenv(occommon.SkipAPIServerTLSWatch) == "true" {
log.Printf("WARNING: APIServer TLS watch not available, using Knative defaults: %v", err)
} else {
log.Fatalf("Failed to set up APIServer TLS watch: %v", err)
}
}

// Read the current TLS profile and inject as WEBHOOK_TLS_* env vars.
// Knative's DefaultConfigFromEnv("WEBHOOK_") inside webhook.New() reads these automatically.
// TLSEnvVarsFromProfile produces "1.2"/"1.3" and comma-separated IANA cipher names —
// exactly the format Knative expects.
if tlsProfile, err := occommon.GetTLSProfileFromAPIServer(signalCtx); err != nil {
log.Printf("WARNING: could not read APIServer TLS profile, using Knative defaults: %v", err)
} else if tlsProfile != nil {
if envVars, err := occommon.TLSEnvVarsFromProfile(tlsProfile); err != nil {
log.Printf("WARNING: could not convert TLS profile, using Knative defaults: %v", err)
} else if envVars != nil {
// Knative only accepts "1.2" or "1.3"; skip if the profile allows older versions
// (e.g. OpenShift "Old" profile uses VersionTLS10). The webhook will then fall
// back to Knative's default minimum of 1.2, which is always safe for admission webhooks.
if envVars.MinVersion == "1.2" || envVars.MinVersion == "1.3" {
os.Setenv("WEBHOOK_TLS_MIN_VERSION", envVars.MinVersion)
}
if envVars.CipherSuites != "" {
os.Setenv("WEBHOOK_TLS_CIPHER_SUITES", envVars.CipherSuites)
}
if envVars.CurvePreferences != "" {
os.Setenv("WEBHOOK_TLS_CURVE_PREFERENCES", envVars.CurvePreferences)
}
}
}

// kwebhook.Options is unchanged — no TLS fields needed.
// Knative reads the WEBHOOK_TLS_* env vars we just set inside webhook.New().
ctx := kwebhook.WithOptions(signalCtx, kwebhook.Options{
ServiceName: serviceName,
Port: 8443,
SecretName: secretName,
})
cfg := injection.ParseAndGetRESTConfigOrDie()
ctx, _ = injection.EnableInjectionOrDie(ctx, cfg)
webhook.CreateWebhookResources(ctx)
webhook.SetTypes("openshift")
Expand Down
135 changes: 135 additions & 0 deletions pkg/reconciler/openshift/common/apiserver_watch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
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 common

import (
"context"
"fmt"
"time"

configv1 "github.com/openshift/api/config/v1"
openshiftconfigclient "github.com/openshift/client-go/config/clientset/versioned"
configinformers "github.com/openshift/client-go/config/informers/externalversions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"knative.dev/pkg/logging"
)

// SkipAPIServerTLSWatch is the env var name used as an escape hatch to suppress
// a fatal error when the APIServer resource is unreachable (e.g. in tests).
// Both the operator controller and the webhook check this variable.
const SkipAPIServerTLSWatch = "SKIP_APISERVER_TLS_WATCH"

// SetupAPIServerTLSWatch creates an OpenShift APIServer informer, registers
// onTLSChange to be called whenever the TLS security profile changes, waits for
// the informer cache to sync, and then sets the shared APIServer lister so that
// GetTLSProfileFromAPIServer works in the calling process.
//
// This function is intentionally generic so it can be used by any process:
// - The operator controller passes impl.EnqueueKey(...) as onTLSChange.
// - The webhook binary passes os.Exit(1) as onTLSChange (restarts to pick up new TLS config).
func SetupAPIServerTLSWatch(ctx context.Context, cfg *rest.Config, onTLSChange func()) error {
logger := logging.FromContext(ctx)

configClient, err := openshiftconfigclient.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("creating OpenShift config client: %w", err)
}

// Verify we can access the APIServer resource before starting the informer.
if _, err := configClient.ConfigV1().APIServers().Get(ctx, "cluster", metav1.GetOptions{}); err != nil {
return fmt.Errorf("accessing APIServer resource: %w", err)
}

// 30-minute resync is sufficient; the watch mechanism handles real-time updates.
factory := configinformers.NewSharedInformerFactory(configClient, 30*time.Minute)
apiServerInformer := factory.Config().V1().APIServers()

if _, err := apiServerInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: func(oldObj, newObj interface{}) {
oldAPIServer, ok := oldObj.(*configv1.APIServer)
if !ok {
return
}
newAPIServer, ok := newObj.(*configv1.APIServer)
if !ok {
return
}
if !APIServerTLSProfileChanged(oldAPIServer, newAPIServer) {
return
}
logger.Info("APIServer TLS security profile changed")
onTLSChange()
},
}); err != nil {
return fmt.Errorf("adding APIServer event handler: %w", err)
}

factory.Start(ctx.Done())

if !cache.WaitForCacheSync(ctx.Done(), apiServerInformer.Informer().HasSynced) {
return fmt.Errorf("failed to sync APIServer informer cache")
}

// Populate the shared lister so GetTLSProfileFromAPIServer works in this process.
SetSharedAPIServerLister(apiServerInformer.Lister(), configClient)
return nil
}

// APIServerTLSProfileChanged reports whether the TLS security profile has changed
// between two APIServer resources.
func APIServerTLSProfileChanged(old, new *configv1.APIServer) bool {
oldProfile := old.Spec.TLSSecurityProfile
newProfile := new.Spec.TLSSecurityProfile

if oldProfile == nil && newProfile == nil {
return false
}
if (oldProfile == nil) != (newProfile == nil) {
return true
}
if oldProfile.Type != newProfile.Type {
return true
}
if oldProfile.Type == configv1.TLSProfileCustomType {
return !customTLSProfilesEqual(oldProfile.Custom, newProfile.Custom)
}
return false
}

// customTLSProfilesEqual compares two custom TLS profile specs for equality.
func customTLSProfilesEqual(old, new *configv1.CustomTLSProfile) bool {
if old == nil && new == nil {
return true
}
if (old == nil) != (new == nil) {
return false
}
if old.MinTLSVersion != new.MinTLSVersion {
return false
}
if len(old.Ciphers) != len(new.Ciphers) {
return false
}
for i := range old.Ciphers {
if old.Ciphers[i] != new.Ciphers[i] {
return false
}
}
return true
}
Loading
Loading