Files
open-cluster-management/pkg/cmd/spoke/operator.go
Tesshu Flower 63632d11a7 🐛 Apply TLS profile to operator and agent serving endpoints (#1639)
* 🐛 Apply TLS profile to spoke agent serving endpoints

The spoke agent binaries (klusterlet-agent, registration-agent, work-agent)
receive --tls-min-version and --tls-cipher-suites flags from the klusterlet
operator via deployment template rendering, but do not wire them to their own
library-go GenericAPIServer serving endpoint on port 8443.

Add ApplyTLSToCommand to all three spoke agent command constructors
(NewKlusterletAgentCmd, NewRegistrationAgent, NewWorkAgent) so the TLS flags
are consumed by the PersistentPreRunE hook and applied to the serving config.

This matches the pattern already used by all hub component commands
(registration-controller, placement, work, addon-manager, grpc-server).

Signed-off-by: Tesshu Flower <tflower@redhat.com>

* 🐛 Apply TLS profile to operator serving endpoints via ConfigMap

The cluster-manager operator (hub) and klusterlet operator (spoke) serve a
health/metrics endpoint on port 8443 via library-go's GenericAPIServer. Unlike
the hub controllers and spoke agents they manage, these operator binaries do not
receive --tls-min-version and --tls-cipher-suites flags from their deployment
manifests -- no external component injects those flags into the operator pods.

Instead, the operators have direct access to the ocm-tls-profile ConfigMap in
their namespace at startup:
- The cluster-manager operator reads it in RunClusterManagerOperator via
  StartTLSConfigMapWatcher and calls os.Exit(0) on changes to restart.
- The klusterlet operator has a tls-profile-sync sidecar that writes the
  ConfigMap from the local OCP APIServer TLS profile and triggers pod restarts.

Add ApplyTLSFromConfigMapToCommand to both operator commands. This installs a
PersistentPreRunE hook that reads the ocm-tls-profile ConfigMap once (using an
in-cluster kube client) before library-go's StartController creates the server,
writes a minimal GenericOperatorConfig YAML to /tmp, and sets --config to point
at it. This ensures the server's TLS config is set correctly from the first
request, using the same restart-on-change mechanism already in place.

The ConfigMap is optional: if not found (upstream deployments without the ACM
sidecar) or if the in-cluster config is unavailable (local dev), the hook is a
no-op and library-go defaults apply.

Also add ApplyTLSToCommand call-site comments to the three spoke agent commands
explaining the difference: agents receive TLS flags from their deployment
manifests (injected by the klusterlet operator), while the operator itself reads
the ConfigMap directly.

Signed-off-by: Tesshu Flower <tflower@redhat.com>

---------

Signed-off-by: Tesshu Flower <tflower@redhat.com>
2026-08-07 01:05:00 +00:00

100 lines
4.4 KiB
Go

package spoke
import (
"context"
"github.com/spf13/cobra"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/utils/clock"
ocmfeature "open-cluster-management.io/api/feature"
commonoptions "open-cluster-management.io/ocm/pkg/common/options"
"open-cluster-management.io/ocm/pkg/features"
"open-cluster-management.io/ocm/pkg/operator/operators/klusterlet"
registration "open-cluster-management.io/ocm/pkg/registration/spoke"
singletonspoke "open-cluster-management.io/ocm/pkg/singleton/spoke"
"open-cluster-management.io/ocm/pkg/version"
work "open-cluster-management.io/ocm/pkg/work/spoke"
)
const agentCmdName = "agent"
// NewKlusterletOperatorCmd generate a command to start klusterlet operator
func NewKlusterletOperatorCmd() *cobra.Command {
opts := commonoptions.NewOptions()
klOptions := klusterlet.Options{}
cmdConfig := opts.
NewControllerCommandConfig("klusterlet", version.Get(), klOptions.RunKlusterletOperator, clock.RealClock{})
cmd := cmdConfig.NewCommandWithContext(context.TODO())
cmd.Use = "klusterlet"
cmd.Short = "Start the klusterlet operator"
// add disable leader election flag
flags := cmd.Flags()
flags.BoolVar(&klOptions.SkipPlaceholderHubSecret, "skip-placeholder-hub-secret", false,
"If set, will skip ensuring a placeholder hub secret which is originally intended for pulling "+
"work image before approved")
if err := flags.MarkDeprecated("skip-placeholder-hub-secret", "flag is not used in the operator."); err != nil {
utilruntime.Must(err)
}
flags.StringVar(&klOptions.ControlPlaneNodeLabelSelector, "control-plane-node-label-selector",
"node-role.kubernetes.io/master=", "control plane node labels, "+
"e.g. 'environment=production', 'tier notin (frontend,backend)'")
flags.Int32Var(&klOptions.DeploymentReplicas, "deployment-replicas", 0,
"Number of deployment replicas, operator will automatically determine replicas if not set")
flags.BoolVar(&klOptions.DisableAddonNamespace, "disable-default-addon-namespace", false,
"If set, will not create default open-cluster-management-agent-addon ns")
flags.BoolVar(&klOptions.EnableSyncLabels, "enable-sync-labels", false,
"If set, will sync the labels of Klusterlet CR to all agent resources")
opts.AddFlags(flags)
// The klusterlet operator does not receive --tls-min-version /
// --tls-cipher-suites flags from its deployment manifest (unlike the spoke
// agent deployments it manages). Instead, the ocm-tls-profile ConfigMap is
// written by the tls-profile-sync sidecar running in the same pod.
// ApplyTLSFromConfigMapToCommand installs a PersistentPreRunE hook that
// reads that ConfigMap once before library-go's StartController starts the
// health/metrics server (port 8443), so the server uses the cluster TLS
// profile from the first request.
// When the ConfigMap changes the tls-profile-sync sidecar causes the pod
// to restart, and this hook re-reads the updated ConfigMap on the next startup.
opts.ApplyTLSFromConfigMapToCommand(cmd)
return cmd
}
// NewKlusterletAgentCmd is to start the singleton agent including registration/work
func NewKlusterletAgentCmd() *cobra.Command {
ctx, cancel := context.WithCancel(context.TODO())
commonOptions := commonoptions.NewAgentOptions()
workOptions := work.NewWorkloadAgentOptions()
registrationOption := registration.NewSpokeAgentOptions()
agentConfig := singletonspoke.NewAgentConfig(commonOptions, registrationOption, workOptions, cancel)
cmdConfig := commonOptions.CommonOpts.
NewControllerCommandConfig("klusterlet-agent", version.Get(), agentConfig.RunSpokeAgent, clock.RealClock{}).
WithHealthChecks(agentConfig.HealthCheckers()...)
cmd := cmdConfig.NewCommandWithContext(ctx)
cmd.Use = agentCmdName
cmd.Short = "Start the klusterlet agent"
flags := cmd.Flags()
commonOptions.AddFlags(flags)
workOptions.AddFlags(flags)
registrationOption.AddFlags(flags)
utilruntime.Must(features.SpokeMutableFeatureGate.Add(ocmfeature.DefaultSpokeRegistrationFeatureGates))
utilruntime.Must(features.SpokeMutableFeatureGate.Add(ocmfeature.DefaultSpokeWorkFeatureGates))
features.SpokeMutableFeatureGate.AddFlag(flags)
// The klusterlet operator injects --tls-min-version and --tls-cipher-suites
// into this agent's deployment manifest from the ocm-tls-profile ConfigMap.
// ApplyTLSToCommand wires those CLI flags to the library-go health/metrics
// server (port 8443) via a PersistentPreRunE hook so the server enforces
// the cluster TLS profile.
commonOptions.CommonOpts.ApplyTLSToCommand(cmd)
return cmd
}