Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5c572cf
fix: add pod/rs guardrail check consistently (#682)
jwtty Apr 28, 2026
34ef330
chore: update token writer file permission (#675)
Arvindthiru Apr 29, 2026
5355ec8
docs: clarify hub-agent cert secret default (#673)
AkashKumar7902 Apr 29, 2026
81ecb5b
fix: deterministic envelope Work names to prevent duplicates (#665)
ytimocin Apr 29, 2026
abfa3ef
fix: member agent validate opts, fix klog.ErrorS, and kubectl-fleet C…
ytimocin Apr 29, 2026
447629b
fix: don't treat in-progress binding states as terminal failures (#670)
ytimocin Apr 30, 2026
97f9ed4
fix: correct TimedWait waitTime regex to reject invalid duration stri…
Copilot May 6, 2026
1c525ee
chore: Move NamespaceWithResourceSelectors to v1 (#697)
weng271190436 May 7, 2026
dd2db6f
refactor: extract IsLatest/LookupLatest policy snapshot helpers (#667)
ytimocin May 9, 2026
444e487
feat: add new error handling utilities (#679)
michaelawyu May 11, 2026
4846c3c
chore: bump github/codeql-action from 4.35.2 to 4.35.3 (#699)
dependabot[bot] May 11, 2026
4a5301f
feat: guardrail: added additional service account protection (#702)
michaelawyu May 12, 2026
359d2b2
chore: update community meetings. (#701)
sjwaight May 12, 2026
e12aa9f
fix: add securityContext to hub-agent and member-agent deployment (#703)
jwtty May 12, 2026
57640d6
test: cover snapshot cross-scope isolation in DeleteResourceSnapshots…
ytimocin May 12, 2026
86dc163
Merge remote-tracking branch 'cncf/main' into backport/v20260512
britaniar May 12, 2026
63da14c
fix: specify which object failed in override error message (#686)
ytimocin May 13, 2026
ce6b31d
fix: address a RO directory issue in templates (#706)
michaelawyu May 13, 2026
1cb3088
Merge remote-tracking branch 'cncf/main' into backport-v20260512
michaelawyu May 13, 2026
9dee67a
Fix failing e2e test now that enableWorkload is False meaning PVC can…
britaniar May 13, 2026
4b121aa
Disable test due to enableWorkload=false change
britaniar May 13, 2026
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
6 changes: 3 additions & 3 deletions .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 # v4

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -69,4 +69,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ build: generate fmt vet ## Build agent binaries
go build -o bin/hubagent cmd/hubagent/main.go
go build -o bin/memberagent cmd/memberagent/main.go
go build -o bin/crdinstaller cmd/crdinstaller/main.go
go build -o bin/kubectl-fleet ./tools/fleet/

.PHONY: run-hubagent
run-hubagent: manifests generate fmt vet ## Run hub-agent from your host
Expand Down
74 changes: 70 additions & 4 deletions apis/placement/v1/clusterresourceplacement_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,15 @@ type ClusterResourcePlacement struct {
}

// PlacementSpec defines the desired state of ClusterResourcePlacement and ResourcePlacement.
// +kubebuilder:validation:XValidation:rule="size(self.resourceSelectors.filter(x, x.kind == 'Namespace' && x.group == \"\" && x.version == 'v1' && has(x.selectionScope) && x.selectionScope == 'NamespaceWithResourceSelectors')) <= 1",message="only one namespace selector with NamespaceWithResourceSelectors mode is allowed"
// +kubebuilder:validation:XValidation:rule="size(self.resourceSelectors.filter(x, x.kind == 'Namespace' && x.group == \"\" && x.version == 'v1' && has(x.selectionScope) && x.selectionScope == 'NamespaceWithResourceSelectors')) == 0 || (size(self.resourceSelectors.filter(x, x.kind == 'Namespace' && x.group == \"\" && x.version == 'v1' && has(x.selectionScope) && x.selectionScope == 'NamespaceWithResourceSelectors' && has(x.name) && size(x.name) > 0 && !has(x.labelSelector))) == 1)",message="namespace selector with NamespaceWithResourceSelectors mode must select by name (not by label)"
// +kubebuilder:validation:XValidation:rule="size(self.resourceSelectors.filter(x, x.kind == 'Namespace' && x.group == \"\" && x.version == 'v1' && has(x.selectionScope) && x.selectionScope == 'NamespaceWithResourceSelectors')) == 0 || size(self.resourceSelectors.filter(x, x.kind == 'Namespace' && x.group == \"\" && x.version == 'v1')) == 1",message="when using NamespaceWithResourceSelectors mode, only one namespace selector is allowed (cannot mix with other namespace selectors)"
type PlacementSpec struct {
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=100

// ResourceSelectors is an array of selectors used to select cluster scoped resources. The selectors are `ORed`.
// You can have 1-100 selectors.
// +kubebuilder:validation:Required
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=100
ResourceSelectors []ResourceSelectorTerm `json:"resourceSelectors"`

// Policy defines how to select member clusters to place the selected resources.
Expand Down Expand Up @@ -185,6 +187,24 @@ type ResourceSelectorTerm struct {
// For ClusterResourcePlacement, you can use SelectionScope to control what gets selected:
// - NamespaceOnly: Only the namespace object itself
// - NamespaceWithResources: The namespace AND all resources within it (default)
// - NamespaceWithResourceSelectors: The namespace AND resources specified by additional selectors
//
// When SelectionScope is NamespaceWithResourceSelectors, you can define additional ResourceSelectorTerms
// (after the namespace selector) to specify which resources to include. These additional selectors can
// target both namespace-scoped resources (within the selected namespace) and cluster-scoped resources.
//
// Important requirements for NamespaceWithResourceSelectors mode:
// - Exactly one namespace selector with this mode is allowed
// - The namespace selector must select by name (not by label)
// - Only one namespace selector is allowed when using this mode (cannot mix with other namespace selectors)
// - All requirements are validated via CEL at API validation time
// - If the selected namespace is deleted after CRP creation, the controller will report an error condition
//
// Example using NamespaceWithResourceSelectors:
// - Namespace selector: {Group: "", Version: "v1", Kind: "Namespace", Name: "prod", SelectionScope: "NamespaceWithResourceSelectors"}
// - Additional selector: {Group: "apps", Version: "v1", Kind: "Deployment", LabelSelector: {app: "frontend"}}
// - Third selector: {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole", Name: "admin"}
// This selects: the "prod" namespace, all Deployments with label app=frontend in "prod", and the "admin" ClusterRole.
//
// +kubebuilder:validation:Required
Kind string `json:"kind"`
Expand All @@ -204,7 +224,7 @@ type ResourceSelectorTerm struct {
// SelectionScope defines the scope of resource selections when the Kind is `namespace`.
// This field is only applicable when Kind is "Namespace" and is ignored for other resource kinds.
// See the Kind field documentation for detailed examples and usage patterns.
// +kubebuilder:validation:Enum=NamespaceOnly;NamespaceWithResources
// +kubebuilder:validation:Enum=NamespaceOnly;NamespaceWithResources;NamespaceWithResourceSelectors
// +kubebuilder:default=NamespaceWithResources
// +kubebuilder:validation:Optional
SelectionScope SelectionScope `json:"selectionScope,omitempty"`
Expand All @@ -230,6 +250,52 @@ const (
// Note: This is the default value. When you select a namespace without specifying SelectionScope,
// this mode is used automatically.
NamespaceWithResources SelectionScope = "NamespaceWithResources"

// NamespaceWithResourceSelectors allows fine-grained selection of specific resources within a namespace.
// The namespace itself is always selected, and you can optionally specify which resources to include
// by adding additional ResourceSelectorTerm entries after the namespace selector.
//
// Use cases:
// 1. Select only specific resource types from a namespace (e.g., only Deployments and Services)
// 2. Select resources matching certain labels within a namespace
// 3. Include specific cluster-scoped resources along with namespace-scoped resources
//
// How "additional selectors" work:
// - Exactly one namespace selector with NamespaceWithResourceSelectors mode is required
// - This selector must select a namespace by name (label selectors not allowed)
// - ADDITIONAL selectors specify which resources to include:
// - Namespace-scoped resources are filtered to only those within the selected namespace
// - Cluster-scoped resources are included as specified (not limited to the namespace)
// - If no additional selectors are provided, only the namespace object itself is selected
//
// Example 1 - Select specific deployments from a namespace:
// Selector 1: {Group: "", Version: "v1", Kind: "Namespace", Name: "production", SelectionScope: "NamespaceWithResourceSelectors"}
// Selector 2: {Group: "apps", Version: "v1", Kind: "Deployment", LabelSelector: {tier: "frontend"}}
// Result: The "production" namespace + all Deployments labeled tier=frontend within "production"
//
// Example 2 - Select namespace with multiple resource types:
// Selector 1: {Group: "", Version: "v1", Kind: "Namespace", Name: "app", SelectionScope: "NamespaceWithResourceSelectors"}
// Selector 2: {Group: "apps", Version: "v1", Kind: "Deployment"}
// Selector 3: {Group: "", Version: "v1", Kind: "Service"}
// Result: The "app" namespace + ALL Deployments and Services within "app"
//
// Example 3 - Include cluster-scoped resources:
// Selector 1: {Group: "", Version: "v1", Kind: "Namespace", Name: "app", SelectionScope: "NamespaceWithResourceSelectors"}
// Selector 2: {Group: "apps", Version: "v1", Kind: "Deployment"}
// Selector 3: {Group: "rbac.authorization.k8s.io", Version: "v1", Kind: "ClusterRole", Name: "app-admin"}
// Result: The "app" namespace + ALL Deployments in "app" + the "app-admin" ClusterRole
//
// Important constraints:
// - Exactly ONE namespace selector with NamespaceWithResourceSelectors mode is allowed
// - The namespace selector must select by name (label selectors not allowed)
// - Only ONE namespace selector total is allowed when using this mode (cannot mix with other namespace selectors)
// - All constraints are enforced via CEL at API validation time
//
// Runtime behavior:
// - If the selected namespace is deleted after the CRP is created, the controller will detect this during
// the next reconciliation and report an error condition in the CRP status
// - The CRP will transition to a failed state until the namespace is recreated or the CRP is updated
NamespaceWithResourceSelectors SelectionScope = "NamespaceWithResourceSelectors"
)

// PlacementPolicy contains the rules to select target member clusters to place the selected resources.
Expand Down
3 changes: 2 additions & 1 deletion apis/placement/v1/stageupdate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,8 @@ type StageTask struct {
Type StageTaskType `json:"type"`

// The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
// +kubebuilder:validation:Pattern="^0|([0-9]+(\\.[0-9]+)?(s|m|h))+$"
// Only hours (h), minutes (m), and seconds (s) units are accepted.
// +kubebuilder:validation:Pattern="^(?:(?:0|[1-9][0-9]*)(\\.[0-9]+)?(?:s|m|h))+$"
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Optional
WaitTime *metav1.Duration `json:"waitTime,omitempty"`
Expand Down
2 changes: 1 addition & 1 deletion apis/placement/v1alpha1/stagedupdate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ type AfterStageTask struct {
Type AfterStageTaskType `json:"type"`

// The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
// +kubebuilder:validation:Pattern="^0|([0-9]+(\\.[0-9]+)?(s|m|h))+$"
// +kubebuilder:validation:Pattern="^(?:0|(?:(?:0|[1-9][0-9]*)(\\.[0-9]+)?(?:s|m|h))+)$"
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Optional
WaitTime metav1.Duration `json:"waitTime,omitempty"`
Expand Down
3 changes: 2 additions & 1 deletion apis/placement/v1beta1/stageupdate_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ type StageTask struct {
Type StageTaskType `json:"type"`

// The time to wait after all the clusters in the current stage complete the update before moving to the next stage.
// +kubebuilder:validation:Pattern="^0|([0-9]+(\\.[0-9]+)?(s|m|h))+$"
// Only hours (h), minutes (m), and seconds (s) units are accepted.
// +kubebuilder:validation:Pattern="^(?:(?:0|[1-9][0-9]*)(\\.[0-9]+)?(?:s|m|h))+$"
// +kubebuilder:validation:Type=string
// +kubebuilder:validation:Optional
WaitTime *metav1.Duration `json:"waitTime,omitempty"`
Expand Down
12 changes: 7 additions & 5 deletions charts/hub-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,20 +164,22 @@ helm install hub-agent oci://ghcr.io/kubefleet-dev/kubefleet/charts/hub-agent \
--create-namespace \
--set useCertManager=true \
--set enableWorkload=true \
--set enableWebhook=true
--set enableWebhook=true \
--set webhookCertSecretName=fleet-webhook-server-cert

# Or using traditional repository
helm install hub-agent kubefleet/hub-agent \
--namespace fleet-system \
--create-namespace \
--set useCertManager=true \
--set enableWorkload=true \
--set enableWebhook=true
--set enableWebhook=true \
--set webhookCertSecretName=fleet-webhook-server-cert
```

The `webhookCertSecretName` parameter specifies the Secret name for the certificate:
- Default: `fleet-webhook-server-cert`
- When using cert-manager, this is where cert-manager stores the certificate
- Default: `unset`; there is no default in `values.yaml`
- When using cert-manager, set this to the Secret where cert-manager stores the certificate
- Must match the secret name referenced in the deployment volume mount

Example with custom secret name:
Expand All @@ -198,4 +200,4 @@ helm install hub-agent kubefleet/hub-agent \
--set useCertManager=true \
--set enableWorkload=true \
--set webhookCertSecretName=my-webhook-secret
```
```
27 changes: 25 additions & 2 deletions charts/hub-agent/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ spec:
{{- include "hub-agent.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "hub-agent.fullname" . }}-sa
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
initContainers:
{{- if .Values.crdInstaller.enabled }}
- name: crd-installer
Expand All @@ -32,6 +39,12 @@ spec:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
args:
- --leader-elect=true
- --enable-webhook={{ .Values.enableWebhook }}
Expand Down Expand Up @@ -92,16 +105,26 @@ spec:
# This path must match FleetWebhookCertDir in pkg/webhook/webhook.go
mountPath: /tmp/k8s-webhook-server/serving-certs
readOnly: true
{{- else }}
volumeMounts:
- name: webhook-cert
# This path must match FleetWebhookCertDir in pkg/webhook/webhook.go.
# Note that this must be mounted one level up from the hardcoded path, otherwise
# the read only root filesystem setup would block the agent from attempting to
# clear the directory.
mountPath: /tmp/k8s-webhook-server/
{{- end }}
{{- if .Values.useCertManager }}
volumes:
- name: webhook-cert
{{- if .Values.useCertManager }}
secret:
secretName: {{ .Values.webhookCertSecretName }}
# defaultMode 0444 (read for all) allows the container process to read the certs
# regardless of the user/group it runs as
defaultMode: 0444
{{- end }}
{{- else }}
emptyDir: {}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
Expand Down
1 change: 1 addition & 0 deletions charts/hub-agent/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enableWorkload: false
useCertManager: false
# webhookCertSecretName is ONLY used when useCertManager=true
# It specifies the name of the Secret where cert-manager stores the certificate
# Example:
# webhookCertSecretName: fleet-webhook-server-cert

forceDeleteWaitTime: 15m0s
Expand Down
19 changes: 19 additions & 0 deletions charts/member-agent/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ spec:
spec:
restartPolicy: Always
serviceAccountName: {{ include "member-agent.fullname" . }}-sa
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
initContainers:
{{- if .Values.crdInstaller.enabled }}
- name: crd-installer
Expand All @@ -29,6 +36,12 @@ spec:
- name: {{ include "member-agent.fullname" . }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
ports:
- name: http
containerPort: 80
Expand Down Expand Up @@ -149,6 +162,12 @@ spec:
- name: refresh-token
image: "{{ .Values.refreshtoken.repository }}:{{ .Values.refreshtoken.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.refreshtoken.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
args:
{{- $provider := .Values.config.provider }}
- {{ $provider }}
Expand Down
12 changes: 10 additions & 2 deletions cmd/memberagent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ func main() {
klog.InfoS("flag:", "name", f.Name, "value", f.Value)
})

if errs := opts.Validate(); len(errs) != 0 {
klog.ErrorS(errs.ToAggregate(), "invalid parameter")
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
}

// Set up controller-runtime logger
ctrl.SetLogger(zap.New(zap.UseDevMode(true)))

Expand Down Expand Up @@ -213,13 +218,16 @@ func buildHubConfig(hubURL string, useCertificateAuth bool, tlsClientInsecure bo
return err
})
if err != nil {
klog.ErrorS(err, "Failed to retrieve token file from the path %s", tokenFilePath)
klog.ErrorS(err, "Failed to retrieve token file", "path", tokenFilePath)
return nil, err
}
hubConfig.BearerTokenFile = tokenFilePath
}

hubConfig.TLSClientConfig.Insecure = tlsClientInsecure
if tlsClientInsecure {
klog.Warning("TLS verification is disabled for hub cluster connection. This is insecure and should not be used in production.")
}
if !tlsClientInsecure {
caBundle, ok := os.LookupEnv("CA_BUNDLE")
if ok && caBundle == "" {
Expand Down Expand Up @@ -257,7 +265,7 @@ func buildHubConfig(hubURL string, useCertificateAuth bool, tlsClientInsecure bo
r := textproto.NewReader(bufio.NewReader(strings.NewReader(header)))
h, err := r.ReadMIMEHeader()
if err != nil && !errors.Is(err, io.EOF) {
klog.ErrorS(err, "Failed to parse HUB_KUBE_HEADER %q", header)
klog.ErrorS(err, "Failed to parse HUB_KUBE_HEADER", "header", header)
return nil, err
}
hubConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
Expand Down
Loading
Loading