Skip to content

Commit 51b75bf

Browse files
diafourzuzzas
andauthored
feat: use hooks as a validating webhook handlers (#223)
feat: use hooks as a validating webhook handlers - new binding type `kubernetesValidating` - add documentation and example - some ci enhancements: build dev image for amd64 only, remove -v from tests Co-authored-by: Andrey Klimentyev <[email protected]>
1 parent 0db09ac commit 51b75bf

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+3289
-269
lines changed

.github/workflows/code-checks.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ jobs:
3939
- name: codespell
4040
run: |
4141
pip install codespell==v1.17.1
42-
codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png" -L witht,eventtypes,uint,uptodate
42+
codespell --skip=".git,go.mod,go.sum,*.log,*.gif,*.png" -L witht,eventtypes,uint,uptodate,keypair
+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
name: Publish dev image amd64
2+
on:
3+
pull_request:
4+
types: [labeled]
5+
env:
6+
# ':robot: build dev image: amd64' label
7+
# not working on job level :-(
8+
# https://github.community/t/how-to-set-and-access-a-workflow-variable/17335/3
9+
LABEL_ID: 2648778919
10+
# build only amd64 to speed up dev image build
11+
BUILDX_PLATFORMS: "linux/amd64"
12+
IMAGE_REPO: flant/shell-operator-dev
13+
14+
jobs:
15+
# Empty job if PR labeled with another label.
16+
stub:
17+
name: Empty job to prevent workflow fail
18+
runs-on: ubuntu-latest
19+
if: github.event_name == 'pull_request' && github.event.label.id != 2648778919
20+
steps:
21+
- name: stub action
22+
run: ": This job is used to prevent the workflow to fail when all other jobs are skipped."
23+
24+
# Remove label from PR.
25+
unlabel:
26+
name: Unlabel
27+
runs-on: ubuntu-latest
28+
if: github.event_name == 'pull_request' && github.event.label.id == 2648778919
29+
steps:
30+
- uses: actions/github-script@v3
31+
with:
32+
github-token: ${{secrets.GITHUB_TOKEN}}
33+
script: |
34+
const eventLabelName = '${{github.event.label.name}}'
35+
const response = await github.issues.listLabelsOnIssue({
36+
owner: context.repo.owner,
37+
repo: context.repo.repo,
38+
issue_number: context.issue.number
39+
})
40+
for (const label of response.data) {
41+
if (label.name === eventLabelName) {
42+
github.issues.removeLabel({
43+
owner: context.repo.owner,
44+
repo: context.repo.repo,
45+
issue_number: context.issue.number,
46+
name: eventLabelName
47+
})
48+
break
49+
}
50+
}
51+
52+
build_dev_image:
53+
name: Dev image
54+
runs-on: ubuntu-latest
55+
if: github.event_name == 'pull_request' && github.event.label.id == 2648778919
56+
steps:
57+
- uses: actions/checkout@v2
58+
59+
- name: Prepare environment
60+
run: |
61+
: Setup imageTag, appVersion and dockerFile envs and build image
62+
imageTag=${GITHUB_REF#refs/tags/}
63+
APP_VERSION=${imageTag}
64+
FINAL_IMAGE_NAME="${IMAGE_REPO}:${imageTag}"
65+
66+
: Override image name and version for dev image
67+
# dev-feat_branch-371e2d3b-2020.02.06_18:37:42
68+
APP_VERSION=${GITHUB_REF#refs/heads/}-${GITHUB_SHA::8}-$(date +'%Y.%m.%d_%H:%M:%S')
69+
FINAL_IMAGE_NAME="${IMAGE_REPO}:pr${{ github.event.pull_request.number }}"
70+
: end override
71+
72+
echo "FINAL_IMAGE_NAME=${FINAL_IMAGE_NAME}" >> ${GITHUB_ENV}
73+
echo "APP_VERSION=${APP_VERSION}" >> ${GITHUB_ENV}
74+
75+
echo "========================================="
76+
echo "APP_VERSION = $APP_VERSION"
77+
echo "FINAL_IMAGE_NAME = $FINAL_IMAGE_NAME"
78+
echo "========================================="
79+
80+
- name: Set up Docker Buildx
81+
id: buildx
82+
uses: docker/setup-buildx-action@v1
83+
with:
84+
version: latest
85+
86+
- name: Login to DockerHub
87+
uses: docker/login-action@v1
88+
with:
89+
username: ${{ secrets.DOCKER_USER }}
90+
password: ${{ secrets.DOCKER_PASS }}
91+
92+
- name: Build and push multi-arch image
93+
run: |
94+
echo "Build $FINAL_IMAGE_NAME with version '$APP_VERSION'"
95+
docker buildx build --push \
96+
--platform $BUILDX_PLATFORMS \
97+
--build-arg appVersion=$APP_VERSION \
98+
--tag $FINAL_IMAGE_NAME .

.github/workflows/publish-dev.yaml

+2-9
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ on:
33
pull_request:
44
types: [labeled]
55
env:
6-
GO111MODULE: on
7-
# build only 2 platforms to speed up dev image build
8-
# TODO create pre-release build label!
96
QEMU_PLATFORMS: arm64,arm
107
BUILDX_PLATFORMS: "linux/amd64,linux/arm64,linux/arm/v7"
118
IMAGE_REPO: flant/shell-operator-dev
@@ -14,19 +11,15 @@ jobs:
1411
stub:
1512
name: Empty job to prevent workflow fail
1613
runs-on: ubuntu-latest
17-
if: github.event_name == 'pull_request' && github.event.label.id != 1838515600 # not ':robot: build dev images' label
14+
if: github.event_name == 'pull_request' && github.event.label.id != 1838515600 # not ':robot: build dev image: multiarch' label
1815
steps:
1916
- name: stub action
2017
run: ": This job is used to prevent the workflow to fail when all other jobs are skipped."
21-
# - name: dump label event
22-
# run: cat $GITHUB_EVENT_PATH
23-
# - name: dump envs
24-
# run: export
2518

2619
unlabel:
2720
name: Unlabel
2821
runs-on: ubuntu-latest
29-
if: github.event_name == 'pull_request' && github.event.label.id == 1838515600 # ':robot: build dev images' label
22+
if: github.event_name == 'pull_request' && github.event.label.id == 1838515600 # ':robot: build dev image: multiarch' label
3023
steps:
3124
- uses: actions/github-script@v3
3225
with:

.github/workflows/tests.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ jobs:
127127
run: |
128128
go test \
129129
-tags test \
130-
-v \
131130
./cmd/... ./pkg/... ./test/utils
132131
133132
prepare_build_dependencies:

BINDING_VALIDATING.md

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# kubernetesValidating
2+
3+
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).
4+
5+
> Note: shell-operator use `admissionregistration.k8s.io/v1`, so Kubernetes 1.16+ is needed.
6+
7+
## Syntax
8+
9+
```yaml
10+
configVersion: v1
11+
onStartup: 10
12+
kubernetes:
13+
- name: myCrdObjects
14+
...
15+
kubernetesValidating:
16+
- name: my-crd-validator.example.com
17+
# include snapshots by binding names
18+
includeSnapshotsFrom: ["myCrdObjects"]
19+
# or use group name to include all snapshots in a group
20+
group: "group name"
21+
labelSelector: # equivalent of objectSelector
22+
matchLabels:
23+
label1: value1
24+
...
25+
namespace:
26+
labelSelector: # equivalent of namespaceSelector
27+
matchLabels:
28+
label1: value1
29+
...
30+
matchExpressions:
31+
- key: environment
32+
operator: In
33+
values: ["prod","staging"]
34+
rules:
35+
- apiVersions:
36+
- v1
37+
apiGroups:
38+
- stable.example.com
39+
resources:
40+
- CronTab
41+
operations:
42+
- "*"
43+
- operations: ["CREATE", "UPDATE"]
44+
apiGroups: ["apps"]
45+
apiVersions: ["v1", "v1beta1"]
46+
resources: ["deployments", "replicasets"]
47+
scope: "Namespaced"
48+
failurePolicy: Ignore | Fail (default)
49+
sideEffects: None (default) | NoneOnDryRun
50+
timeoutSeconds: 2 (default is 10)
51+
```
52+
53+
## Parameters
54+
55+
- `name` — a required parameter. It should be a domain with at least three segments separated by dots.
56+
57+
- `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.
58+
59+
- `group` — a key to include snapshots from a group of `schedule` and `kubernetes` bindings. See [grouping](#an-example-of-a-binding-context-with-group).
60+
61+
- `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).
62+
63+
- `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).
64+
65+
- `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).
66+
67+
- `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`.
68+
69+
- `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`.
70+
71+
- `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).
72+
73+
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:
74+
- `objectSelector` is a `labelSelector` as in the `kubernetes` binding.
75+
- `namespaceSelector` is a `namespace.labelSelector` as in the `kubernetes` binding.
76+
- `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.
77+
- `matchPolicy` is always "Equivalent". See [Matching requests: matchPolicy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-matchpolicy).
78+
- there are additional fields `group` and `includeSnapshotsFrom` to include snapshots in the binding context.
79+
80+
## Example
81+
82+
```
83+
configVersion: v1
84+
kubernetesValidating:
85+
- name: private-repo-policy.example.com
86+
rules:
87+
- apiGroups: ["stable.example.com"]
88+
apiVersions: ["v1"]
89+
operations: ["CREATE"]
90+
resources: ["crontabs"]
91+
scope: "Namespaced"
92+
```
93+
94+
The Shell-operator will execute hook with this configuration on every creation of CronTab object.
95+
96+
See example [204-validating-webhook](./examples/204-validating-webhook).
97+
98+
## Hook input and output
99+
100+
> 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"`.
101+
102+
The hook receives a binding context and should return response in `$VALIDATING_RESPONSE_PATH`.
103+
104+
$BINDING_CONTEXT_PATH file example:
105+
106+
```yaml
107+
[{
108+
# Name as defined in binding configuration.
109+
"binding": "my-crd-validator.example.com",
110+
# Validating to distinguish from other events.
111+
"type": "Validating",
112+
# Snapshots as defined by includeSnapshotsFrom or group.
113+
"snapshots": { ... }
114+
# AdmissionReview object.
115+
"review": {
116+
"apiVersion": "admission.k8s.io/v1",
117+
"kind": "AdmissionReview",
118+
"request": {
119+
# Random uid uniquely identifying this admission call
120+
"uid": "705ab4f5-6393-11e8-b7cc-42010a800002",
121+
122+
# Fully-qualified group/version/kind of the incoming object
123+
"kind": {"group":"autoscaling","version":"v1","kind":"Scale"},
124+
# Fully-qualified group/version/kind of the resource being modified
125+
"resource": {"group":"apps","version":"v1","resource":"deployments"},
126+
# subresource, if the request is to a subresource
127+
"subResource": "scale",
128+
129+
# Fully-qualified group/version/kind of the incoming object in the original request to the API server.
130+
# This only differs from `kind` if the webhook specified `matchPolicy: Equivalent` and the
131+
# original request to the API server was converted to a version the webhook registered for.
132+
"requestKind": {"group":"autoscaling","version":"v1","kind":"Scale"},
133+
# Fully-qualified group/version/kind of the resource being modified in the original request to the API server.
134+
# This only differs from `resource` if the webhook specified `matchPolicy: Equivalent` and the
135+
# original request to the API server was converted to a version the webhook registered for.
136+
"requestResource": {"group":"apps","version":"v1","resource":"deployments"},
137+
# subresource, if the request is to a subresource
138+
# This only differs from `subResource` if the webhook specified `matchPolicy: Equivalent` and the
139+
# original request to the API server was converted to a version the webhook registered for.
140+
"requestSubResource": "scale",
141+
142+
# Name of the resource being modified
143+
"name": "my-deployment",
144+
# Namespace of the resource being modified, if the resource is namespaced (or is a Namespace object)
145+
"namespace": "my-namespace",
146+
147+
# operation can be CREATE, UPDATE, DELETE, or CONNECT
148+
"operation": "UPDATE",
149+
150+
"userInfo": {
151+
# Username of the authenticated user making the request to the API server
152+
"username": "admin",
153+
# UID of the authenticated user making the request to the API server
154+
"uid": "014fbff9a07c",
155+
# Group memberships of the authenticated user making the request to the API server
156+
"groups": ["system:authenticated","my-admin-group"],
157+
# Arbitrary extra info associated with the user making the request to the API server.
158+
# This is populated by the API server authentication layer and should be included
159+
# if any SubjectAccessReview checks are performed by the webhook.
160+
"extra": {
161+
"some-key":["some-value1", "some-value2"]
162+
}
163+
},
164+
165+
# object is the new object being admitted.
166+
# It is null for DELETE operations.
167+
"object": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
168+
# oldObject is the existing object.
169+
# It is null for CREATE and CONNECT operations.
170+
"oldObject": {"apiVersion":"autoscaling/v1","kind":"Scale",...},
171+
# options contains the options for the operation being admitted, like meta.k8s.io/v1 CreateOptions, UpdateOptions, or DeleteOptions.
172+
# It is null for CONNECT operations.
173+
"options": {"apiVersion":"meta.k8s.io/v1","kind":"UpdateOptions",...},
174+
175+
# dryRun indicates the API request is running in dry run mode and will not be persisted.
176+
# Webhooks with side effects should avoid actuating those side effects when dryRun is true.
177+
# See http://k8s.io/docs/reference/using-api/api-concepts/#make-a-dry-run-request for more details.
178+
"dryRun": false
179+
}
180+
}
181+
}]
182+
```
183+
184+
Response example:
185+
```
186+
cat <<EOF > $VALIDATING_RESPONSE_PATH
187+
{"allowed": true}
188+
EOF
189+
```
190+
191+
Deny object creation and explain why:
192+
```
193+
cat <<EOF > $VALIDATING_RESPONSE_PATH
194+
{"allowed": false, "message": "You cannot do this because it is Tuesday and your name starts with A"}
195+
EOF
196+
```
197+
198+
User will see an error message:
199+
200+
```
201+
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
202+
```
203+
204+
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.
205+
206+
## HTTP server and Kubernetes configuration
207+
208+
Shell-operator should create an HTTP endpoint with TLS support and register endpoints in the ValidatingWebhookConfiguration resource.
209+
210+
There should be a Service for shell-operator (see [Availability](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#availability)).
211+
212+
Command line options:
213+
214+
```
215+
--validating-webhook-server-cert="/validating-certs/cert.crt"
216+
A path to a server certificate for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_SERVER_CERT.
217+
--validating-webhook-server-key="/validating-certs/cert.key"
218+
A path to a server private key for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_SERVER_KEY.
219+
--validating-webhook-ca="/validating-certs/ca.crt"
220+
A path to a ca bundle for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_CA.
221+
--validating-webhook-client-ca=VALIDATING-WEBHOOK-CLIENT-CA ...
222+
A path to a server certificate for ValidatingWebhook. Can be set with $VALIDATING_WEBHOOK_CLIENT_CA.
223+
--validating-webhook-service-name=VALIDATING-WEBHOOK-SERVICE-NAME ...
224+
A name of a service in front of a shell-operator. Can be set with $VALIDATING_WEBHOOK_SERVICE_NAME.
225+
```

0 commit comments

Comments
 (0)