# Adding Admission Control

One thing we can do with our operator is add in _admission control_. In the grafana-app-sdk, admission control refers to performing validation and/or mutation on incoming requests to the API server. Since our operator doesn't receive these requests, we have to expose a webhook for the API server to call when it receives a request, and then register that webhook with the API server. Luckily, this is not quite so complicated as it sounds. Let's add some validation and some mutation to our `Issue` kind using our operator. We want to do the following when an `Issue` is created or updated:
1. Check that the `title` isn't empty
2. Add a `status` label that matches the `spec.status`, to allow us to filter our list requests by `status`
For (1), we need to perform validation, and for (2), we need to do a mutation. So lets add validating and mutating controllers to our operator.

## Validation

Luckily, with the `simple.App` we're using that was generated by `component add operator`, adding validation and mutation for a kind is relatively simple. Let's add the following after line 25:
```go
issueValidator := &simple.Validator{
    ValidateFunc: func(ctx context.Context, request *app.AdmissionRequest) error {
        cast, ok := request.Object.(*issuev1alpha1.Issue)
        if !ok {
            return fmt.Errorf("object is not of type *issue.Issue (%s %s)", request.Object.GetName(), request.Object.GroupVersionKind().String())
        }
        if strings.Trim(cast.Spec.Title, " ") == "" {
            return fmt.Errorf("spec.title must not be empty or consist only of whitespace characters")
        }
        return nil
    },
}
```
And then we can reference our new validator in our `ManagedKinds` list in the config, updating 
```go
ManagedKinds: []simple.AppManagedKind{
    {
        Kind:    issuev1alpha1.Kind(),
        Watcher: issueWatcher,
    },
},
```
to
```go
ManagedKinds: []simple.AppManagedKind{
    {
        Kind:    issuev1alpha1.Kind(),
        Watcher: issueWatcher,
        Validator: issueValidator,
    },
},
```
For each `AppManagedKind`, We can supply additional features or options, not just the kind and watcher, but for now we'll just add validation and mutation.

To add validation, we need something that implements `simple.KindValidator`, which is defined as 
```go
// KindValidator is an interface which describes an object which can validate a kind, used in AppManagedKind
type KindValidator interface {
	Validate(context.Context, *app.AdmissionRequest) error
}
```
Basically, it consumes an admission request and produces a validation error if validation fails, or returns nil on success. 
The `simple` package has a ready-to-go implementation for this: `simple.Validator`, which calls `ValidateFunc` when `Validate` is called. 
That's what we're using here, but you can always define your own type to implement `KindValidator`, too. 

Just like with `runner.WatchKind`, `runner.ValidateKind` takes the kind, and then the object to apply to it. In this case, to validate a kind, we need a `ValidatingAdmissionController`. This is a simple one-method interface, which we could define a type for ourselves, but we can also use `resource.SimpleValidatingAdmissionController` as a default implementation. In `SimpleValidatingAdmissionController`, `ValidateFunc` is called by the `Validate` function, so we just need to define our validation function in `ValidateFunc`.

`Validate` receives a context, and a request or type `*resource.AdmissionRequest`. The request contains some meta information that we can ignore for now (including action type, user information, and GroupVersionKind), and the `Object` from the request (and an `OldObject` which may be nil if there was not a previous state of the object, depending on the action). We can use this `Object` value to check if the updated or created object has an empty title, and return an error. An important thing to note is that  text of the `error` returned by `Validate` will be returned to the end-user, so make sure it's as descriptive as necessary for the user to fix their request. If no error is returned by `Validate`, the validation passes.

If we re-built our operator now and attempted to run it locally, our validation wouldn't occur, and we'd actually get a startup error:
```bash
{"time":"2024-10-02T15:28:02.56268893Z","level":"INFO","msg":"Starting operator"}
{"time":"2024-10-02T15:28:02.562828305Z","level":"ERROR","msg":"Operator exited with error","error":"kind Issue/v1 does not support validation, but has a validator"}
```
What does this mean? Well, it comes from this line in `pkg/app/app.go`:
```go
// Validate the capabilities against the provided manifest to make sure there isn't a mismatch
err = a.ValidateManifest(cfg.ManifestData)
```
What happened here, is that we added a validator, but our app's manifest doesn't list validation as supported for that kind. 
This means that the operator runner wouldn't expose a validation webhook, and we'd have to deal with a lot more debugging attempting to find out what is going on. 
So the `ValidateManifest` call makes it easy to see if there's a mismatch between our _claimed_ capabilities (the app manifest), 
and the ones we have declared in-code.

To fix this, we just need to update our manifest. Right now, the manifest is generated from our kinds' CUE definitions, so that means we need to update our kind to specify that it supports validation.
```shell
curl -o kinds/issue.cue https://raw.githubusercontent.com/grafana/grafana-app-sdk/main/docs/tutorials/issue-tracker/cue/issue-v3.cue
```
Here we've updated the `issuev1alpha1` object to include:
```cue
// Validation is used when generating the manifest to indicate support for validation admission control
// for this kind. Here, we list that we want to do validation for CREATE and UPDATE operations.
validation: operations: ["CREATE","UPDATE"]
```
Now if we re-run 
```shell
make generate
```
The new manifest will indicate support for validation. Now all we have to do is re-build our operator and update our local setup:
```shell
make local/down && make build/operator && make local/push_operator && make local/up
```
Now, if we attempt to make an issue with an empty Title (or one only consisting of spaces), we get an error:
```
admission webhook "issuetrackerproject-app-operator.default.svc" denied the request: spec.title must not be empty or consist only of whitespace characters
```
But, if we add any non-whitespace characters, we see that we can still successfully make the `issue`.

## Mutation

Next, we want to make sure that there is a label named `status` that always matches `spec.status` for each object. We can do this by adding a `Mutator` to our `AppManagedKind`. 
Let's add an `issueMutator` below our `issueValidator`:
```go
issueMutator := &simple.Mutator{
    MutateFunc: func(ctx context.Context, request *app.AdmissionRequest) (*app.MutatingResponse, error) {
        cast, ok := request.Object.(*issuev1alpha1.Issue)
        if !ok {
            return nil, fmt.Errorf("object is not of type *issue.Issue (%s %s)", request.Object.GetName(), request.Object.GroupVersionKind().String())
        }
        if cast.Labels == nil {
            cast.Labels = make(map[string]string)
        }
        cast.Labels["status"] = cast.Spec.Status
        return &app.MutatingResponse{
            UpdatedObject: cast,
        }, nil
    },
}
```
Then we add it to our `AppManagedKind` like so:
```go
{
    Kind:      issuev1alpha1.Kind(),
    Watcher:   issueWatcher,
    Validator: issueValidator,
    Mutator:   issueMutator,
},
```
Again, we need to make sure our manifest matches our code, so let's update the kind definition:
```shell
curl -o kinds/issue.cue https://raw.githubusercontent.com/grafana/grafana-app-sdk/main/docs/tutorials/issue-tracker/cue/issue-v4.cue
```
(this adds `mutation: operations: ["CREATE","UPDATE"]` to our `issuev1alpha1` object)

Now we just need to re-generate the manifest, re-build our operator, and re-deploy:
```shell
make local/down && make generate && make build/operator && make local/push_operator && make local/up
```
Now, whenever you make or update an issue, you'll see that it has a label attached for its status. You can see these in the network tab of your browser console, or via `kubectl`. In fact, you can now filter issues using kubectl:
```
kubectl get issues -l status=open
```
Will give you only issues with a status of `open`, for example.

The neat part about this validation and mutation is that it occurs irrespective of how the user decides to create or update their issues. Since the webhooks are called by the API server, if the user uses `kubectl`, `flux`, or the plugin UI, they all will route to the same API server backend, which will call our validating and mutating webhooks.

For more details on webhooks and admission control, see [Admission Control](../../admission-control.md).

### Prev: [Writing Operator Code](07-operator-watcher.md)
### Next: [Wrap-Up and Further Reading](09-wrap-up.md)