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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions config/openshift/base/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,12 @@ rules:
- delete
- update
- patch
# to read APIServer TLS profile for injecting TLS configuration into components
- apiGroups:
- config.openshift.io
resources:
- apiservers
verbs:
- get
- list
- watch
11 changes: 11 additions & 0 deletions pkg/reconciler/common/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ type Extension interface {
Finalize(context.Context, v1alpha1.TektonComponent) error
}

// TLSProfileFingerprinter is an optional interface for extensions that support
// TLS profile detection from external sources (e.g., OpenShift APIServer).
// Extensions implementing this interface enable their reconcilers to detect
// external TLS configuration changes and trigger re-reconciliation.
type TLSProfileFingerprinter interface {
// GetTLSProfileFingerprint returns a deterministic string representing the current
// platform's TLS security profile state. Returns empty string if TLS profile
// is not configured.
GetTLSProfileFingerprint(context.Context) string
}

// ExtensionGenerator creates an Extension from a Context
type ExtensionGenerator func(context.Context) Extension

Expand Down
22 changes: 19 additions & 3 deletions pkg/reconciler/kubernetes/tektonresult/tektonresult.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,26 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tr *v1alpha1.TektonResul
} else {
// If target namespace and version are not changed then check if spec
// of TektonResult is changed by checking hash stored as annotation on
// TektonInstallerSet with computing new hash of TektonResult Spec
// TektonInstallerSet with computing new hash of TektonResult Spec.
// Also include external TLS profile state to trigger updates when
// platform TLS configuration changes.
logger.Debug("Checking for spec changes in TektonResult")
// Hash of TektonResult Spec
expectedSpecHash, err := hash.Compute(tr.Spec)

// Get platform TLS profile fingerprint if extension supports it (empty on vanilla k8s)
var tlsFingerprint string
if fp, ok := r.extension.(common.TLSProfileFingerprinter); ok {
tlsFingerprint = fp.GetTLSProfileFingerprint(ctx)
}

// Hash of TektonResult Spec combined with TLS fingerprint
type hashInput struct {
Spec v1alpha1.TektonResultSpec
TLSFingerprint string
}
expectedSpecHash, err := hash.Compute(hashInput{
Spec: tr.Spec,
TLSFingerprint: tlsFingerprint,
})
if err != nil {
logger.Errorw("Failed to compute spec hash", "error", err)
return err
Expand Down
191 changes: 191 additions & 0 deletions pkg/reconciler/openshift/common/apiserver_watch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
Copyright 2025 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"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"knative.dev/pkg/controller"
"knative.dev/pkg/logging"
)

// ResourceLister is an interface for listing Kubernetes resources
// This allows the watch to work with any Tekton component type
type ResourceLister interface {
List(selector labels.Selector) ([]ResourceWithName, error)
}

// ResourceWithName is an interface for resources that have a name
type ResourceWithName interface {
GetName() string
}

// SetupAPIServerTLSWatch sets up a watch on the OpenShift APIServer resource
// to monitor TLS security profile changes. When changes are detected, it enqueues
// the component's resources for reconciliation. Returns an error only for unexpected
// failures; returns nil if APIServer is unavailable.
func SetupAPIServerTLSWatch(
ctx context.Context,
restConfig *rest.Config,
impl *controller.Impl,
lister ResourceLister,
componentName string,
) error {
logger := logging.FromContext(ctx).With("component", componentName)

// Create OpenShift config client
configClient, err := openshiftconfigclient.NewForConfig(restConfig)
if err != nil {
logger.Errorf("Failed to create OpenShift config client: %v", err)
return fmt.Errorf("failed to create OpenShift config client: %w", err)
}

// Check if we can access the APIServer resource
_, err = configClient.ConfigV1().APIServers().Get(ctx, "cluster", metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
// APIServer resource doesn't exist - TLS watch is not available
logger.Info("APIServer 'cluster' resource not found, TLS profile watch disabled")
return nil
}
// Real error - log and return
logger.Errorf("Failed to access APIServer resource: %v", err)
return fmt.Errorf("failed to access APIServer resource: %w", err)
}

// Create a shared informer factory for OpenShift config resources
configInformerFactory := configinformers.NewSharedInformerFactory(configClient, 10*time.Minute)

// Get the APIServer informer
apiServerInformer := configInformerFactory.Config().V1().APIServers()

// Add event handler to watch for APIServer changes
if _, err := apiServerInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: func(oldObj, newObj interface{}) {
oldAPIServer, ok := oldObj.(*configv1.APIServer)
if !ok {
logger.Warn("Failed to cast old object to APIServer")
return
}
newAPIServer, ok := newObj.(*configv1.APIServer)
if !ok {
logger.Warn("Failed to cast new object to APIServer")
return
}

// Check if TLS security profile actually changed
if !tlsProfileChanged(oldAPIServer, newAPIServer) {
logger.Debug("APIServer updated but TLS profile unchanged, skipping reconciliation")
return
}

logger.Infof("APIServer TLS security profile changed, triggering %s reconciliation", componentName)

resources, err := lister.List(labels.Everything())
if err != nil {
logger.Errorf("Failed to list %s resources after APIServer change: %v", componentName, err)
return
}

for _, resource := range resources {
logger.Infof("Enqueuing %s %s for reconciliation due to APIServer TLS change",
componentName, resource.GetName())
impl.EnqueueKey(types.NamespacedName{Name: resource.GetName()})
}
},
}); err != nil {
return fmt.Errorf("failed to add APIServer event handler: %w", err)
}

// Start the informer factory
configInformerFactory.Start(ctx.Done())

// Wait for caches to sync
logger.Info("Waiting for APIServer informer cache to sync...")
if !cache.WaitForCacheSync(ctx.Done(), apiServerInformer.Informer().HasSynced) {
return fmt.Errorf("failed to wait for APIServer informer cache to sync")
}
logger.Info("APIServer informer cache synced successfully")

return nil
}

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

// Both nil - no change
if oldProfile == nil && newProfile == nil {
return false
}

// One nil, one not - changed
if (oldProfile == nil) != (newProfile == nil) {
return true
}

// Different types - changed
if oldProfile.Type != newProfile.Type {
return true
}

// For custom profiles, check the actual settings
if oldProfile.Type == configv1.TLSProfileCustomType {
return !customProfilesEqual(oldProfile.Custom, newProfile.Custom)
}

// For predefined profiles (Old, Intermediate, Modern), type change is sufficient
return false
}

// customProfilesEqual checks if two custom TLS profiles are equal.
// TODO(openshift/api#2583): Add curve preferences comparison once the field is added to TLSProfileSpec.
func customProfilesEqual(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