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
51 changes: 51 additions & 0 deletions pkg/utils/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright 2025 The KubeFleet 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 utils

import (
"slices"

authenticationv1 "k8s.io/api/authentication/v1"
)

// This file contains constants and utility functions related to Azure-specific logic.
// We define these in a separate file to avoid potential merge conflicts from the KubeFleet repository when backporting changes.
const (
// ReconcileLabelKey is the label key added to resources that should be reconciled.
// The value indicates the reason the resource should be reconciled.
ReconcileLabelKey = "fleet.azure.com/reconcile"
// ReconcileLabelValue is the label value paired with ReconcileLabelKey,
// indicating the resource is managed and should be reconciled.
ReconcileLabelValue = "managed"

// AKSServiceUserName is the username whose deployment requests should be mutated.
AKSServiceUserName = "aksService"
// SystemMastersGroup is the Kubernetes group representing cluster administrators.
SystemMastersGroup = "system:masters"
)

// IsAKSService reports whether the user is the aksService user with
// system:masters group membership.
func IsAKSService(userInfo authenticationv1.UserInfo) bool {
return userInfo.Username == AKSServiceUserName && slices.Contains(userInfo.Groups, SystemMastersGroup)
}

// HasReconcileLabel reports whether the given labels contain the fleet reconcile
// label key with a non-empty value.
func HasReconcileLabel(labels map[string]string) bool {
return labels[ReconcileLabelKey] != ""
}
3 changes: 3 additions & 0 deletions pkg/webhook/add_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"go.goms.io/fleet/pkg/webhook/clusterresourceplacement"
"go.goms.io/fleet/pkg/webhook/clusterresourceplacementdisruptionbudget"
"go.goms.io/fleet/pkg/webhook/clusterresourceplacementeviction"
"go.goms.io/fleet/pkg/webhook/deployment"
"go.goms.io/fleet/pkg/webhook/fleetresourcehandler"
"go.goms.io/fleet/pkg/webhook/membercluster"
"go.goms.io/fleet/pkg/webhook/pdb"
Expand All @@ -29,4 +30,6 @@ func init() {
AddToManagerFuncs = append(AddToManagerFuncs, resourceoverride.Add)
AddToManagerFuncs = append(AddToManagerFuncs, clusterresourceplacementeviction.Add)
AddToManagerFuncs = append(AddToManagerFuncs, clusterresourceplacementdisruptionbudget.Add)
AddToManagerFuncs = append(AddToManagerFuncs, deployment.AddMutating)
AddToManagerFuncs = append(AddToManagerFuncs, deployment.Add)
}
90 changes: 90 additions & 0 deletions pkg/webhook/deployment/mutating_webhook.go
Comment thread
jwtty marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
Copyright 2025 The KubeFleet Authors.
Copy link
Copy Markdown
Contributor

@michaelawyu michaelawyu Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Wantong! Just to double-check: do we need to mutate the replicaSet as well? Otherwise, the Deployments won't function correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Sorry if I remembered this wrong 🙈)


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 deployment

import (
"context"
"encoding/json"
"fmt"
"net/http"

appsv1 "k8s.io/api/apps/v1"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"go.goms.io/fleet/pkg/utils"
)

// MutatingPath is the webhook service path for mutating Deployment resources.
var MutatingPath = fmt.Sprintf(utils.MutatingPathFmt, appsv1.SchemeGroupVersion.Group, appsv1.SchemeGroupVersion.Version, "deployment")

type deploymentMutator struct {
decoder webhook.AdmissionDecoder
}

// AddMutating registers the mutating webhook for Deployments with the manager.
func AddMutating(mgr manager.Manager) error {
hookServer := mgr.GetWebhookServer()
hookServer.Register(MutatingPath, &webhook.Admission{Handler: &deploymentMutator{decoder: admission.NewDecoder(mgr.GetScheme())}})
return nil
}

// Handle injects the fleet.azure.com/reconcile=managed label onto the
// Deployment and its pod template when the request originated from the aksService user.
func (m *deploymentMutator) Handle(_ context.Context, req admission.Request) admission.Response {
klog.V(2).InfoS("handling deployment mutating webhook",
"operation", req.Operation, "namespace", req.Namespace, "name", req.Name, "user", req.UserInfo.Username)

// Pass through requests targeting reserved system namespaces.
if utils.IsReservedNamespace(req.Namespace) {
return admission.Allowed(fmt.Sprintf("namespace %s is a reserved system namespace, no mutation needed", req.Namespace))
}

// Only mutate when the request was made by the aksService user with
// system:masters group membership.
if !utils.IsAKSService(req.UserInfo) {
return admission.Allowed("user is not aksService, no mutation needed")
}

var deploy appsv1.Deployment
if err := m.decoder.Decode(req, &deploy); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

// Add the reconcile label on the Deployment metadata.
if deploy.Labels == nil {
deploy.Labels = map[string]string{}
}
deploy.Labels[utils.ReconcileLabelKey] = utils.ReconcileLabelValue

// Add the reconcile label on the pod template metadata.
if deploy.Spec.Template.Labels == nil {
deploy.Spec.Template.Labels = map[string]string{}
}
deploy.Spec.Template.Labels[utils.ReconcileLabelKey] = utils.ReconcileLabelValue

marshaled, err := json.Marshal(deploy)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}

klog.V(2).InfoS("mutated deployment with reconcile label",
"operation", req.Operation, "namespace", req.Namespace, "name", req.Name)
return admission.PatchResponseFromRaw(req.Object.Raw, marshaled)
}
Loading
Loading