diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da3926..717439e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ All notable changes to this project will be documented in this file. -## Unreleased +## 0.9.0 - 2020-01-09 +* 41: Introduce support for cluster-wide resources * 36: Update last-applied-configuration annotation when patching a resource * 18: Implement /v1/dryrun API diff --git a/README.md b/README.md index 80418c2..9358fac 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,11 @@ Use KubeMod to: * [Modification of metadata](#modification-of-metadata) * [Sidecar injection](#sidecar-injection) * [Resource rejection](#resource-rejection) -* [Understanding ModRules](#understanding-modrules) +* [Anatomy of a ModRule](#anatomy-of-a-modrule) * [Match section](#match-section) * [Patch section](#patch-section) * [Miscellaneous](#miscellaneous) + * [Namespaced and cluster-wide resources](#namespaced-and-cluster-wide-resources) * [Note on idempotency](#note-on-idempotency) * [Debugging ModRules](#debugging-modrules) * [Declarative kubectl apply](#declarative-kubectl-apply) @@ -343,9 +344,9 @@ spec: --- -## Understanding ModRules +## Anatomy of a ModRule -A `ModRule` has a `type`, a `match` section, and a `patch` section. +A `ModRule` consists of a `type`, a `match` section, and a `patch` section. ```yaml apiVersion: api.kubemod.io/v1beta1 @@ -732,6 +733,14 @@ The field is a Golang template evaluated in the context of the object being reje ## Miscellaneous +### Namespaced and cluster-wide resources + +KubeMod can patch/reject both namespaced and cluster-wide resources. + +If a ModRule is deployed to any namespace other than `kubemod-system`, the ModRule applies only to objects deployed/updated in that same namespace. + +ModRules deployed to namespace `kubemod-system` apply to cluster-wide resources such as `Namespace` or `ClusterRole`. + ### Note on idempotency Make sure your patch ModRules are idempotent — executing them multiple times against the same object should lead to no changes beyond the first execution. diff --git a/app/operatorapp.go b/app/operatorapp.go index a3c3d5c..fbaff3f 100644 --- a/app/operatorapp.go +++ b/app/operatorapp.go @@ -77,10 +77,10 @@ func NewKubeModOperatorApp( } // NewControllerManager instantiates a new controller manager. -func NewControllerManager(scheme *runtime.Scheme, metricsAddr string, enableLeaderElection EnableLeaderElection, log logr.Logger) (manager.Manager, error) { +func NewControllerManager(scheme *runtime.Scheme, metricsAddr OperatorMetricsAddr, enableLeaderElection EnableLeaderElection, log logr.Logger) (manager.Manager, error) { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, - MetricsBindAddress: metricsAddr, + MetricsBindAddress: string(metricsAddr), Port: 9443, LeaderElection: bool(enableLeaderElection), LeaderElectionID: "f950e141.kubemod.io", diff --git a/app/wire.go b/app/wire.go index a60ad7e..a17de2b 100644 --- a/app/wire.go +++ b/app/wire.go @@ -27,10 +27,12 @@ import ( type EnableLeaderElection bool type EnableDevModeLog bool +type OperatorMetricsAddr string func InitializeKubeModOperatorApp( scheme *runtime.Scheme, - metricsAddr string, + metricsAddr OperatorMetricsAddr, + clusterModRulesNamespace controllers.ClusterModRulesNamespace, enableLeaderElection EnableLeaderElection, log logr.Logger) (*KubeModOperatorApp, error) { wire.Build( diff --git a/app/wire_gen.go b/app/wire_gen.go index d4947fd..2ab4b3b 100644 --- a/app/wire_gen.go +++ b/app/wire_gen.go @@ -15,7 +15,7 @@ import ( // Injectors from wire.go: -func InitializeKubeModOperatorApp(scheme *runtime.Scheme, metricsAddr string, enableLeaderElection EnableLeaderElection, log logr.Logger) (*KubeModOperatorApp, error) { +func InitializeKubeModOperatorApp(scheme *runtime.Scheme, metricsAddr OperatorMetricsAddr, clusterModRulesNamespace controllers.ClusterModRulesNamespace, enableLeaderElection EnableLeaderElection, log logr.Logger) (*KubeModOperatorApp, error) { manager, err := NewControllerManager(scheme, metricsAddr, enableLeaderElection, log) if err != nil { return nil, err @@ -23,7 +23,7 @@ func InitializeKubeModOperatorApp(scheme *runtime.Scheme, metricsAddr string, en language := expressions.NewJSONPathLanguage() modRuleStoreItemFactory := core.NewModRuleStoreItemFactory(language, log) modRuleStore := core.NewModRuleStore(modRuleStoreItemFactory, log) - modRuleReconciler, err := controllers.NewModRuleReconciler(manager, modRuleStore, log) + modRuleReconciler, err := controllers.NewModRuleReconciler(manager, modRuleStore, clusterModRulesNamespace, log) if err != nil { return nil, err } @@ -50,3 +50,5 @@ func InitializeKubeModWebApp(webAppAddr string, enableDevModeLog EnableDevModeLo type EnableLeaderElection bool type EnableDevModeLog bool + +type OperatorMetricsAddr string diff --git a/controllers/modrule_controller.go b/controllers/modrule_controller.go index 12887c9..f429d56 100644 --- a/controllers/modrule_controller.go +++ b/controllers/modrule_controller.go @@ -28,22 +28,27 @@ import ( "github.com/kubemod/kubemod/core" ) +// ClusterModRulesNamespace is a type of string used by DI to inject the namespace where cluster-wide ModRules are deployed. +type ClusterModRulesNamespace string + // ModRuleReconciler reconciles a ModRule object type ModRuleReconciler struct { - client client.Client - log logr.Logger - scheme *runtime.Scheme - modRuleStore *core.ModRuleStore + client client.Client + log logr.Logger + scheme *runtime.Scheme + modRuleStore *core.ModRuleStore + clusterModRulesNamespace string } // NewModRuleReconciler creates a new ModRuleReconciler. -func NewModRuleReconciler(manager manager.Manager, modRuleStore *core.ModRuleStore, log logr.Logger) (*ModRuleReconciler, error) { +func NewModRuleReconciler(manager manager.Manager, modRuleStore *core.ModRuleStore, clusterModRulesNamespace ClusterModRulesNamespace, log logr.Logger) (*ModRuleReconciler, error) { reconciler := &ModRuleReconciler{ - client: manager.GetClient(), - log: log.WithName("controllers").WithName("modrule"), - scheme: manager.GetScheme(), - modRuleStore: modRuleStore, + client: manager.GetClient(), + log: log.WithName("controllers").WithName("modrule"), + scheme: manager.GetScheme(), + modRuleStore: modRuleStore, + clusterModRulesNamespace: string(clusterModRulesNamespace), } return reconciler, nil @@ -58,12 +63,20 @@ func (r *ModRuleReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.log.WithValues("modrule", req.NamespacedName) + storeNamespace := req.Namespace + + // If the ModRule is stored in the cluster-wide namespace, store the ModRule under an empty namespace + // in order to match non-namespaced resources. + if storeNamespace == r.clusterModRulesNamespace { + storeNamespace = "" + } + if err := r.client.Get(ctx, req.NamespacedName, &modRule); err != nil { // If the modrule is not found, then it has been deleted. if apierrors.IsNotFound(err) { // Delete the ModRule from the ModRule memory store. - r.modRuleStore.Delete(req.Namespace, req.Name) + r.modRuleStore.Delete(storeNamespace, req.Name) } else { log.Error(err, "unable to fetch ModRule") } @@ -73,7 +86,9 @@ func (r *ModRuleReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { return ctrl.Result{}, client.IgnoreNotFound((err)) } - // Store the new ModRule in our memory store. + // Store the new ModRule in our memory store, but before we do, clear the namespace + // in case the ModRule is deployed to the cluster-wide namespace. + modRule.Namespace = storeNamespace r.modRuleStore.Put(&modRule) log.V(1).Info("Successfully stored ModRule") diff --git a/core/dragnetwebhook.go b/core/dragnetwebhook.go index 15be560..816ba17 100644 --- a/core/dragnetwebhook.go +++ b/core/dragnetwebhook.go @@ -48,8 +48,16 @@ func NewDragnetWebhookHandler(manager manager.Manager, modRuleStore *ModRuleStor func (h *DragnetWebhookHandler) Handle(ctx context.Context, req admission.Request) admission.Response { log := h.log.WithValues("request uid", req.UID, "namespace", req.Namespace, "resource", fmt.Sprintf("%v/%v", req.Resource.Resource, req.Name), "operation", req.Operation) + storeNamespace := req.Namespace + // If the target object is a namespace, UPDATE operations will pass in the namespace itself as the owner of the namespace. + // This is misleading - namespaces are cluster-wide objects. + // Set the storeNamespace to an empty string to reflect that and pick up the cluster-wide modrules. + if req.Kind.Group == "" && req.Kind.Version == "v1" && req.Kind.Kind == "Namespace" { + storeNamespace = "" + } + // First run patch operations. - patchedJSON, patch, err := h.modRuleStore.CalculatePatch(req.Namespace, req.Object.Raw, log) + patchedJSON, patch, err := h.modRuleStore.CalculatePatch(storeNamespace, req.Object.Raw, log) if err != nil { log.Error(err, "Failed to calculate patch") @@ -58,7 +66,7 @@ func (h *DragnetWebhookHandler) Handle(ctx context.Context, req admission.Reques } // Then test the result against the set of relevant Reject rules. - rejections := h.modRuleStore.DetermineRejections(req.Namespace, patchedJSON, log) + rejections := h.modRuleStore.DetermineRejections(storeNamespace, patchedJSON, log) if len(rejections) > 0 { rejectionMessages := strings.Join(rejections, ",") diff --git a/go.sum b/go.sum index 34e3bba..12c5efc 100644 --- a/go.sum +++ b/go.sum @@ -531,6 +531,7 @@ k8s.io/apiextensions-apiserver v0.18.6 h1:vDlk7cyFsDyfwn2rNAO2DbmUbvXy5yT5GE3rrq k8s.io/apiextensions-apiserver v0.18.6/go.mod h1:lv89S7fUysXjLZO7ke783xOwVTm6lKizADfvUM/SS/M= k8s.io/apimachinery v0.18.6 h1:RtFHnfGNfd1N0LeSrKCUznz5xtUP1elRGvHJbL3Ntag= k8s.io/apimachinery v0.18.6/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= +k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ= k8s.io/apiserver v0.18.6/go.mod h1:Zt2XvTHuaZjBz6EFYzpp+X4hTmgWGy8AthNVnTdm3Wg= k8s.io/client-go v0.18.6 h1:I+oWqJbibLSGsZj8Xs8F0aWVXJVIoUHWaaJV3kUN/Zw= k8s.io/client-go v0.18.6/go.mod h1:/fwtGLjYMS1MaM5oi+eXhKwG+1UHidUEXRh6cNsdO0Q= diff --git a/main.go b/main.go index 92739f5..c493a76 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,7 @@ import ( apiv1beta1 "github.com/kubemod/kubemod/api/v1beta1" "github.com/kubemod/kubemod/app" + "github.com/kubemod/kubemod/controllers" // +kubebuilder:scaffold:imports ) @@ -46,8 +47,9 @@ type Config struct { RunOperator bool RunWebApp bool - WebAppAddr string - OperatorMetricsAddr string + WebAppAddr string + OperatorMetricsAddr string + ClusterModRulesNamespace string EnableLeaderElection bool EnableDevModeLog bool @@ -62,6 +64,7 @@ func main() { flag.StringVar(&config.WebAppAddr, "webapp-addr", ":8081", "The address the web app binds to.") flag.StringVar(&config.OperatorMetricsAddr, "operator-metrics-addr", ":8082", "The address the operator metric endpoint binds to.") + flag.StringVar(&config.ClusterModRulesNamespace, "cluster-modrules-namespace", "kubemod-system", "The namespace where cluster-wide ModRules are deployed.") flag.BoolVar(&config.EnableLeaderElection, "enable-leader-election", false, "Enable leader election for KubeMod operator. "+ "Enabling this will ensure there is only one active controller manager.") @@ -98,7 +101,8 @@ func run(config *Config) error { if config.RunOperator { _, err := app.InitializeKubeModOperatorApp( scheme, - config.OperatorMetricsAddr, + app.OperatorMetricsAddr(config.OperatorMetricsAddr), + controllers.ClusterModRulesNamespace(config.ClusterModRulesNamespace), app.EnableLeaderElection(config.EnableLeaderElection), log) diff --git a/samples/modrules/modrule-4.yaml b/samples/modrules/modrule-4.yaml new file mode 100644 index 0000000..a593c54 --- /dev/null +++ b/samples/modrules/modrule-4.yaml @@ -0,0 +1,19 @@ +apiVersion: api.kubemod.io/v1beta1 +kind: ModRule +metadata: + name: patch-istio-namespace + namespace: kubemod-system +spec: + type: Patch + + match: + - select: '$.kind' + matchValue: 'Namespace' + + - select: '$.metadata.name' + matchValue: 'my-namespace' + + patch: + - op: add + path: /metadata/labels/istio.io~1rev + value: canary diff --git a/samples/modrules/modrule-5.yaml b/samples/modrules/modrule-5.yaml new file mode 100644 index 0000000..882439a --- /dev/null +++ b/samples/modrules/modrule-5.yaml @@ -0,0 +1,19 @@ +apiVersion: api.kubemod.io/v1beta1 +kind: ModRule +metadata: + name: patch-clusterrole + namespace: kubemod-system +spec: + type: Patch + + match: + - select: '$.kind' + matchValue: 'ClusterRole' + + - select: '$.metadata.name' + matchValue: 'my-clusterrole' + + patch: + - op: add + path: /metadata/labels/color + value: blue diff --git a/samples/stack/my-clusterrole.yaml b/samples/stack/my-clusterrole.yaml new file mode 100644 index 0000000..4f4ae1f --- /dev/null +++ b/samples/stack/my-clusterrole.yaml @@ -0,0 +1,4 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: my-clusterrole diff --git a/samples/stack/my-namespace.yaml b/samples/stack/my-namespace.yaml new file mode 100644 index 0000000..43f149c --- /dev/null +++ b/samples/stack/my-namespace.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: my-namespace + labels: + color: blue