Skip to content

feat: use hooks as a validating webhook handlers #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jan 12, 2021
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/code-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ jobs:
- name: codespell
run: |
pip install codespell==v1.17.1
codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png" -L witht,eventtypes,uint,uptodate
codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png" -L witht,eventtypes,uint,uptodate,keypair
98 changes: 98 additions & 0 deletions .github/workflows/publish-dev-amd64.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Publish dev image amd64
on:
pull_request:
types: [labeled]
env:
# ':robot: build dev image: amd64' label
# not working on job level :-(
# https://github.community/t/how-to-set-and-access-a-workflow-variable/17335/3
LABEL_ID: 2648778919
# build only amd64 to speed up dev image build
BUILDX_PLATFORMS: "linux/amd64"
IMAGE_REPO: flant/shell-operator-dev

jobs:
# Empty job if PR labeled with another label.
stub:
name: Empty job to prevent workflow fail
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.label.id != 2648778919
steps:
- name: stub action
run: ": This job is used to prevent the workflow to fail when all other jobs are skipped."

# Remove label from PR.
unlabel:
name: Unlabel
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.label.id == 2648778919
steps:
- uses: actions/github-script@v3
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const eventLabelName = '${{github.event.label.name}}'
const response = await github.issues.listLabelsOnIssue({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
})
for (const label of response.data) {
if (label.name === eventLabelName) {
github.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: eventLabelName
})
break
}
}

build_dev_image:
name: Dev image
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.label.id == 2648778919
steps:
- uses: actions/checkout@v2

- name: Prepare environment
run: |
: Setup imageTag, appVersion and dockerFile envs and build image
imageTag=${GITHUB_REF#refs/tags/}
APP_VERSION=${imageTag}
FINAL_IMAGE_NAME="${IMAGE_REPO}:${imageTag}"

: Override image name and version for dev image
# dev-feat_branch-371e2d3b-2020.02.06_18:37:42
APP_VERSION=${GITHUB_REF#refs/heads/}-${GITHUB_SHA::8}-$(date +'%Y.%m.%d_%H:%M:%S')
FINAL_IMAGE_NAME="${IMAGE_REPO}:pr${{ github.event.pull_request.number }}"
: end override

echo "FINAL_IMAGE_NAME=${FINAL_IMAGE_NAME}" >> ${GITHUB_ENV}
echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}

echo "========================================="
echo "APP_VERSION = $APP_VERSION"
echo "FINAL_IMAGE_NAME = $FINAL_IMAGE_NAME"
echo "========================================="

- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
with:
version: latest

- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}

- name: Build and push multi-arch image
run: |
echo "Build $FINAL_IMAGE_NAME with version '$APP_VERSION'"
docker buildx build --push \
--platform $BUILDX_PLATFORMS \
--build-arg appVersion=$APP_VERSION \
--tag $FINAL_IMAGE_NAME .
11 changes: 2 additions & 9 deletions .github/workflows/publish-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ on:
pull_request:
types: [labeled]
env:
GO111MODULE: on
# build only 2 platforms to speed up dev image build
# TODO create pre-release build label!
QEMU_PLATFORMS: arm64,arm
BUILDX_PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7"
IMAGE_REPO: flant/shell-operator-dev
Expand All @@ -14,19 +11,15 @@ jobs:
stub:
name: Empty job to prevent workflow fail
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.label.id != 1838515600 # not ':robot: build dev images' label
if: github.event_name == 'pull_request' && github.event.label.id != 1838515600 # not ':robot: build dev image: multiarch' label
steps:
- name: stub action
run: ": This job is used to prevent the workflow to fail when all other jobs are skipped."
# - name: dump label event
# run: cat $GITHUB_EVENT_PATH
# - name: dump envs
# run: export

unlabel:
name: Unlabel
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && github.event.label.id == 1838515600 # ':robot: build dev images' label
if: github.event_name == 'pull_request' && github.event.label.id == 1838515600 # ':robot: build dev image: multiarch' label
steps:
- uses: actions/github-script@v3
with:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ jobs:
run: |
go test \
-tags test \
-v \
./cmd/... ./pkg/... ./test/utils

prepare_build_dependencies:
Expand Down
225 changes: 225 additions & 0 deletions BINDING_VALIDATING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# kubernetesValidating

This binding transforms a hook into a handler for ValidatingWebhookConfiguration. The Shell-operator creates ValidatingWebhookConfiguration, starts HTTPS server, and runs hooks to handle [AdmissionReview requests](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#request).

> Note: shell-operator use `admissionregistration.k8s.io/v1`, so Kubernetes 1.16+ is needed.

## Syntax

```yaml
configVersion: v1
onStartup: 10
kubernetes:
- name: myCrdObjects
...
kubernetesValidating:
- name: my-crd-validator.example.com
# include snapshots by binding names
includeSnapshotsFrom: ["myCrdObjects"]
# or use group name to include all snapshots in a group
group: "group name"
labelSelector: # equivalent of objectSelector
matchLabels:
label1: value1
...
namespace:
labelSelector: # equivalent of namespaceSelector
matchLabels:
label1: value1
...
matchExpressions:
- key: environment
operator: In
values: ["prod","staging"]
rules:
- apiVersions:
- v1
apiGroups:
- stable.example.com
resources:
- CronTab
operations:
- "*"
- operations: ["CREATE", "UPDATE"]
apiGroups: ["apps"]
apiVersions: ["v1", "v1beta1"]
resources: ["deployments", "replicasets"]
scope: "Namespaced"
failurePolicy: Ignore | Fail (default)
sideEffects: None (default) | NoneOnDryRun
timeoutSeconds: 2 (default is 10)
```

## Parameters

- `name` — a required parameter. It should be a domain with at least three segments separated by dots.

- `includeSnapshotsFrom` — an array of names of `kubernetes` bindings in a hook. When specified, a list of monitored objects from these bindings will be added to the binding context in the `snapshots` field.

- `group` — a key to include snapshots from a group of `schedule` and `kubernetes` bindings. See [grouping](#an-example-of-a-binding-context-with-group).

- `labelSelector` — [standard](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#labelselector-v1-meta) selector of objects by labels (examples [of use](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels)). See [objectSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector).

- `namespace.labelSelector` — this filter works like `labelSelector` but for namespaces. See [namespaceSelector](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector).

- `rules` — a required list of rules used to determine if a request to the Kubernetes API server should be sent to the hook. See [Rules](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-rules).

- `failurePolicy` — defines how errors from the hook are handled. See [Failure policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy). Default is `Fail`.

- `sideEffects` — determine whether the hook is `dryRun`-aware. See [side effects](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#side-effects) documentation. Default is `None`.

- `timeoutSeconds` — a seconds API server should wait for a hook to respond before treating the call as a failure. See [timeouts](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#timeouts). Default is 10 (seconds).

As you can see, it is the close copy of a [Webhook configuration](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-configuration). Differences are:
- `objectSelector` is a `labelSelector` as in the `kubernetes` binding.
Copy link
Member

Choose a reason for hiding this comment

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

Not so important, but this works only in Kubernetes 1.15+

Copy link
Contributor Author

Choose a reason for hiding this comment

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

- `namespaceSelector` is a `namespace.labelSelector` as in the `kubernetes` binding.
- `clientConfig` is managed by the Shell-operator. You should provide a Service for the Shell-operator HTTPS endpoint. See example [204-validating-webhook](./examples/204-validating-webhook) for possible solution.
- `matchPolicy` is always "Equivalent". See [Matching requests: matchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy).
- there are additional fields `group` and `includeSnapshotsFrom` to include snapshots in the binding context.

## Example

```
configVersion: v1
kubernetesValidating:
- name: private-repo-policy.example.com
rules:
- apiGroups: ["stable.example.com"]
apiVersions: ["v1"]
operations: ["CREATE"]
resources: ["crontabs"]
scope: "Namespaced"
```

The Shell-operator will execute hook with this configuration on every creation of CronTab object.

See example [204-validating-webhook](./examples/204-validating-webhook).

## Hook input and output

> Note that the `group` parameter is only for including snapshots. `kubernetesValidating` hook is never executed on `schedule` or `kubernetes` events with binding context with `"type":"Group"`.

The hook receives a binding context and should return response in `$VALIDATING_RESPONSE_PATH`.

$BINDING_CONTEXT_PATH file example:

```yaml
[{
# Name as defined in binding configuration.
"binding": "my-crd-validator.example.com",
# Validating to distinguish from other events.
"type": "Validating",
# Snapshots as defined by includeSnapshotsFrom or group.
"snapshots": { ... }
# AdmissionReview object.
"review": {
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"request": {
# Random uid uniquely identifying this admission call
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",

# Fully-qualified group/version/kind of the incoming object
"kind": {"group":"autoscaling","version":"v1","kind":"Scale"},
# Fully-qualified group/version/kind of the resource being modified
"resource": {"group":"apps","version":"v1","resource":"deployments"},
# subresource, if the request is to a subresource
"subResource": "scale",

# Fully-qualified group/version/kind of the incoming object in the original request to the API server.
# This only differs from `kind` if the webhook specified `matchPolicy: Equivalent` and the
# original request to the API server was converted to a version the webhook registered for.
"requestKind": {"group":"autoscaling","version":"v1","kind":"Scale"},
# Fully-qualified group/version/kind of the resource being modified in the original request to the API server.
# This only differs from `resource` if the webhook specified `matchPolicy: Equivalent` and the
# original request to the API server was converted to a version the webhook registered for.
"requestResource": {"group":"apps","version":"v1","resource":"deployments"},
# subresource, if the request is to a subresource
# This only differs from `subResource` if the webhook specified `matchPolicy: Equivalent` and the
# original request to the API server was converted to a version the webhook registered for.
"requestSubResource": "scale",

# Name of the resource being modified
"name": "my-deployment",
# Namespace of the resource being modified, if the resource is namespaced (or is a Namespace object)
"namespace": "my-namespace",

# operation can be CREATE, UPDATE, DELETE, or CONNECT
"operation": "UPDATE",

"userInfo": {
# Username of the authenticated user making the request to the API server
"username": "admin",
# UID of the authenticated user making the request to the API server
"uid": "014fbff9a07c",
# Group memberships of the authenticated user making the request to the API server
"groups": ["system:authenticated","my-admin-group"],
# Arbitrary extra info associated with the user making the request to the API server.
# This is populated by the API server authentication layer and should be included
# if any SubjectAccessReview checks are performed by the webhook.
"extra": {
"some-key":["some-value1", "some-value2"]
}
},

# object is the new object being admitted.
# It is null for DELETE operations.
"object": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
# oldObject is the existing object.
# It is null for CREATE and CONNECT operations.
"oldObject": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
# options contains the options for the operation being admitted, like meta.k8s.io/v1 CreateOptions, UpdateOptions, or DeleteOptions.
# It is null for CONNECT operations.
"options": {"apiVersion":"meta.k8s.io/v1","kind":"UpdateOptions",...},

# dryRun indicates the API request is running in dry run mode and will not be persisted.
# Webhooks with side effects should avoid actuating those side effects when dryRun is true.
# See http://k8s.io/docs/reference/using-api/api-concepts/#make-a-dry-run-request for more details.
"dryRun": false
}
}
}]
```

Response example:
```
cat <<EOF > $VALIDATING_RESPONSE_PATH
{"allowed": true}
EOF
```

Deny object creation and explain why:
```
cat <<EOF > $VALIDATING_RESPONSE_PATH
{"allowed": false, "message": "You cannot do this because it is Tuesday and your name starts with A"}
EOF
```

User will see an error message:

```
Error from server: admission webhook "policy.example.com" denied the request: You cannot do this because it is Tuesday and your name starts with A
```

Empty or invalid $VALIDATING_RESPONSE_PATH file is considered as `"allowed": false` with a short message about the problem and a more verbose error in the log.

## HTTP server and Kubernetes configuration

Shell-operator should create an HTTP endpoint with TLS support and register endpoints in the ValidatingWebhookConfiguration resource.

There should be a Service for shell-operator (see [Availability](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#availability)).

Command line options:

```
--validating-webhook-server-cert="/validating-certs/cert.crt"
A path to a server certificate for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_SERVER_CERT.
--validating-webhook-server-key="/validating-certs/cert.key"
A path to a server private key for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_SERVER_KEY.
--validating-webhook-ca="/validating-certs/ca.crt"
A path to a ca bundle for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_CA.
--validating-webhook-client-ca=VALIDATING-WEBHOOK-CLIENT-CA ...
A path to a server certificate for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_CLIENT_CA.
--validating-webhook-service-name=VALIDATING-WEBHOOK-SERVICE-NAME ...
A name of a service in front of a shell-operator. Can be set with $VALIDATING_WEBHOOK_SERVICE_NAME.
```
Loading