Skip to content

Commit

Permalink
Implement support for cluster level resources (#42)
Browse files Browse the repository at this point in the history
* Implement support for cluster level resources
  • Loading branch information
vassilvk authored Jan 9, 2021
1 parent f320011 commit 60f380a
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 25 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions app/operatorapp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 3 additions & 1 deletion app/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions app/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 26 additions & 11 deletions controllers/modrule_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
Expand All @@ -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")
Expand Down
12 changes: 10 additions & 2 deletions core/dragnetwebhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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, ",")
Expand Down
1 change: 1 addition & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 7 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
Expand All @@ -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.")
Expand Down Expand Up @@ -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)

Expand Down
19 changes: 19 additions & 0 deletions samples/modrules/modrule-4.yaml
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions samples/modrules/modrule-5.yaml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 4 additions & 0 deletions samples/stack/my-clusterrole.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: my-clusterrole
6 changes: 6 additions & 0 deletions samples/stack/my-namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
labels:
color: blue

0 comments on commit 60f380a

Please sign in to comment.