Skip to content

Adding a New Resource

Peter Matseykanets edited this page Jan 23, 2024 · 3 revisions

Adding a New Resource

For the below steps, rbac.authorization.k8s.io/roles will be used as the example.

Codegen

Before any other step, you should add the following to generate helpful functions for use in the webhook. In pkg/codegen/main.go we need to add our new resource to generateObjectsFromRequest. This function takes a map[string]args.Group. The key is the resource group, and the values are slices of the objects to generate functions for.

...
"rbac.authorization.k8s.io": {
Types: []interface{}{
    &rbacv1.Role{},
    &rbacv1.RoleBinding{},
},
...

This will generate two useful functions <Resource>OldAndNewFromRequest and <Resource>FromRequest which are helper functions that return <Resource> objects from the raw JSON found in the Request. (Role example)

Note: If we will need a controller for the resource, we also need to run the controller generation. Add the resource to controllergen.Run under Groups in the same way.

controllergen.Run(args.Options{
...
    Groups: map[string]args.Group{
        "management.cattle.io": {
            Types: []interface{}{
                v3.Cluster{},
...

This generates the controller that we can use to perform actions on those resources. (GlobalRole example)

To generate files, simply run go generate ./... at the root of the repo.

Create the webhook

The next step is to create a webhook that performs the validation or mutation we want. These are added in a new directory under pkg/resources/<resource group>/<API version>/<resource>. So for Roles, it can be found in pkg/resources/rbac.authorization.k8s.io/v1/role. Validators go in validator.go and Mutators go in mutator.go.

To add a new Webhook that we need to implement a WebhookHandler, more specifically, either a ValidatingAdmissionHandler or a MutatingAdmissionHandler.

NewValidator

While the function NewValidator and NewMutator are not necessary for the implementation of either ValidatingAdmissionHandler or MutatingAdmissionHandler, it is convention to add it to be used in handlers.go as discussed below in "Handler". For Roles, the NewValidator() has no inputs. It would be just as valid to create a Validator directly &role.Validator{}. However, when the validator needs access to controllers, resolvers, etc. passing it via the NewValidator function is used. For example, GlobalRoles has the following NewValidator:

// NewValidator returns a new validator used for validation globalRoles.
func NewValidator(ruleResolver validation.AuthorizationRuleResolver, grbResolver *resolvers.GRBClusterRuleResolver,
	sar authorizationv1.SubjectAccessReviewInterface) *Validator {
	return &Validator{
		admitter: admitter{
			resolver:    ruleResolver,
			grbResolver: grbResolver,
			sar:         sar,
		},
	}
}

LabelSelectors

When implementing the function ValidatingWebhook, it is possible to add LabelSelectors to the returned webhook.ObjectSelector. This informs the webhook that when validating an object, only validate objects that have the given label. This is extremely valuable when adding validation to a frequently modified object (ie core types) to reduce the performance impact. Every additional webhook introduces a potential bottleneck, so strive to limit the objects validated.

For Roles, we select only Roles that have the label authz.management.cattle.io/gr-owner (link):

webhook.ObjectSelector = &metav1.LabelSelector{
    MatchExpressions: []metav1.LabelSelectorRequirement{
        {
            Key:      grOwnerLabel,
            Operator: metav1.LabelSelectorOpExists,
        },
    },
}

More information on Label Selectors

Admit

This function is where the bulk of the work happens. This passes in a Request and returns a Response and an error. Any validating logic falls into this function and returns return admission.ResponseAllowed(), nil if the requested change is allowed. Any other return, such as return nil, err or return admission.ResponseBadRequest(), nil denies the request and prevents the caller from modifying the resource.

Tests

Unit tests

Each resource's webhook should have a reasonable, preferably total, unit test coverage.

Integration tests

For each resource's webhook there should be at least a minimal integration test residing in tests/integration.

To run integration tests

export KUBECONFIG=<cluster to test on>
go test ./tests/integration -run IntegrationTest

Documentation

For every resource, documentation must be added describing what validation and mutation occurs for the resource. This is done in a markdown file (eg Role.md). Please make sure to detail every check being made.

Once you've added all the information related to validation and mutation, run go generate ./... at the root of the repo. This will add your newly created docs to docs.md, which is the complete collection of all validation and mutation that occurs in rancher/webhook.

Handler

The final step of adding a new resource webhook is to add the Validator to the list of handlers. In pkg/server/handlers.go, create the validator roles := role.NewValidator() and append it to the list of handlers.

MultiClusterManagement

In pkg/server/handlers.go, you'll notice that many of the handlers are only added if clients.MultiClusterManagement is true. These are webhooks that only get added to the local cluster, and not any downstream clusters. This is almost always where new webhooks get added. Unless there is a very urgent use case, NEVER make webhooks on downstream clusters. This could have potentially enormous performance ramifications. Only do so when absolutely necessary (ie security reasons). If you're unsure, either default to adding it to only local (inside if clients.MultiClusterManagement) or ask someone more familiar with the webhook.