diff --git a/.github/workflows/assign-class-label.yaml b/.github/workflows/assign-class-label.yaml new file mode 100644 index 0000000..f6553aa --- /dev/null +++ b/.github/workflows/assign-class-label.yaml @@ -0,0 +1,43 @@ +name: assign-class-label tests and linting/formatting + +on: + push: + paths: + - container-images/assign-class-label/** + pull_request: + paths: + - container-images/assign-class-label/** + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + working-directory: ./container-images/assign-class-label/ + run: | + pip install -r requirements.txt + pip install -r test-requirements.txt + + - name: Run ruff format (formatting) + working-directory: ./container-images/assign-class-label/ + run: | + ruff format + + - name: Run ruff check (linting) + working-directory: ./container-images/assign-class-label/ + run: | + ruff check --fix + + - name: Run tests + working-directory: ./container-images/assign-class-label/ + run: | + pytest tests/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 68bc17f..956529e 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,8 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + + +webhook.crt +webhook.key +secret.yaml \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..aa292ea --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,18 @@ +[lint] +# 1. Enable flake8-bugbear (B) rules, in addition to the defaults. +select = ["E4", "E7", "E9", "F", "B"] + +# 2. Avoid enforcing line-length violations (E501) +ignore = ["E501"] + +# 3. Avoid trying to fix flake8-bugbear (B) violations. +unfixable = ["B"] + +# 4. Ignore E402 (import violations) in all __init__.py files, and in selected subdirectories. +[lint.per-file-ignores] +"__init__.py" = ["E402"] +"**/{tests,docs,tools}/*" = ["E402"] + +[format] +# 5. Use single quotes in ruff format. +quote-style = "single" \ No newline at end of file diff --git a/README.md b/README.md index f542d99..df7e673 100644 --- a/README.md +++ b/README.md @@ -100,4 +100,71 @@ This script is used to retrieve the URL for a particular notebook associated wit ``` Enter the notebook name: xxx URL for notebook xxx: xxx - ``` \ No newline at end of file + ``` + +## Webhooks + +### assign-class-label + +This script is used to add labels to the pod of a user denoting which class they belong to (class="classname"). This allows us to differentiate between users of different classes running in the same namespace. This also allows us to create validating [gatekeeper policies](https://github.com/OCP-on-NERC/gatekeeper) for each class. + +Before using the assign-class-label webhook, the group-sync cronjob should be run so that the users of the different classes are added to their respective groups in openshift. + +In order to modify the deployment follow these steps: + +1. Modify the GROUPS env variable to contain the list of classes (openshift groups) of which you would like to assign class labels. This file is found here: webhooks/assign-class-label/deployment.yaml + +2. Generate a new OpenSSL certificate + + ``` + openssl req -x509 -sha256 -newkey rsa:2048 -keyout webhook.key -out webhook.crt -days 1024 -nodes -addext "subjectAltName = DNS.1:service_name.namespace.svc" + ``` + + When deployed to rhods-notebooks the command was specified as such: + + ``` + openssl req -x509 -sha256 -newkey rsa:2048 -keyout webhook.key -out webhook.crt -days 1024 -nodes -addext "subjectAltName = DNS.1:assign-class-label-webhook.rhods-notebooks.svc" + ``` + +3. Add the cert and key to the required resources: + + ``` + cat webhook.crt | base64 | tr -d '\n' + ``` + + ``` + cat webhook.key | base64 | tr -d '\n' + ``` + + This will encode the certificate and key in base64 format which is required. Copy the output of the webhook.crt to the caBundle in webhooks/assign-class-label/webhook-config.yaml. Then create a secret.yaml that looks like this + + ``` + apiVersion: v1 + kind: Secret + metadata: + name: webhook-cert + type: Opaque + data: + webhook.crt: + webhook.key: + ``` + + Copy and paste the output of the cat command to the respective fields for webhook.crt and webhook.key. Then execute + + ``` + oc apply -f secret.yaml --as system:admin + ``` + + within the same namespace that your webhook will be deployed to. + + +4. Change namespace variable in the kubernetes manifests to match namespace you want the webhook to be deployed to. + +5. From webhooks/assign-class-label/ directory run: +``` + oc apply -k . --as system:admin +``` + +***Steps 2, 3, and 4 are only required if you are deploying to a new namespace/environment.*** + +The python script and docker image used for the webserver should not need changes made to it. But in the case that changes must be made, the Dockerfile and python script can be found at docker/src/python/assign-class-label/. diff --git a/container-images/assign-class-label/Dockerfile b/container-images/assign-class-label/Dockerfile new file mode 100644 index 0000000..b362378 --- /dev/null +++ b/container-images/assign-class-label/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app/ + +COPY requirements.txt ./ + +RUN pip install -r requirements.txt + +COPY . ./ + +EXPOSE 5000 + +CMD ["gunicorn", "wsgi:webhook", "--log-level=info", "--workers", "3", "--bind", "0.0.0.0:5000", "--keyfile", "/certs/webhook.key", "--certfile", "/certs/webhook.crt"] diff --git a/container-images/assign-class-label/conftest.py b/container-images/assign-class-label/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/container-images/assign-class-label/models.py b/container-images/assign-class-label/models.py new file mode 100644 index 0000000..d318b9f --- /dev/null +++ b/container-images/assign-class-label/models.py @@ -0,0 +1,37 @@ +from pydantic import BaseModel, constr +from typing import Dict, Optional + + +class PodMetadata(BaseModel): + labels: Optional[Dict[str, str]] = None + + +class PodObject(BaseModel): + metadata: PodMetadata + + +class AdmissionRequest(BaseModel): + uid: constr(min_length=1) + object: PodObject + + +class AdmissionReview(BaseModel): + request: AdmissionRequest + + +class Status(BaseModel): + message: Optional[str] = None + + +class AdmissionResponse(BaseModel): + uid: str + allowed: bool + status: Optional[Status] = None + patchType: Optional[str] = None + patch: Optional[str] = None + + +class AdmissionReviewResponse(BaseModel): + apiVersion: str = 'admission.k8s.io/v1' + kind: str = 'AdmissionReview' + response: AdmissionResponse diff --git a/container-images/assign-class-label/mutate.py b/container-images/assign-class-label/mutate.py new file mode 100644 index 0000000..9b67759 --- /dev/null +++ b/container-images/assign-class-label/mutate.py @@ -0,0 +1,165 @@ +import logging +import json +import base64 +from flask import Flask, request, jsonify, Response +from kubernetes import config, client +from openshift.dynamic import DynamicClient + +from pydantic import ValidationError +from typing import Any, List + +from models import AdmissionReviewResponse, AdmissionReview, AdmissionResponse, Status + +LOG = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def decode_pod_user(pod_user: str) -> str: + return pod_user.replace('-40', '@').replace('-2e', '.') + + +def get_client() -> DynamicClient: + try: + config.load_config() + k8s_client = client.ApiClient() + dyn_client = DynamicClient(k8s_client) + return dyn_client + except config.ConfigException as e: + LOG.error('Could not configure Kubernetes client: %s', str(e)) + exit(1) + + +def get_group_resource(dyn_client): + return dyn_client.resources.get(api_version='user.openshift.io/v1', kind='Group') + + +# Get users of a given group +def get_group_members(group_resource: Any, group_name: str) -> List[str]: + group_obj = group_resource.get(name=group_name) + return group_obj.users + + +def assign_class_label( + pod: dict[str, Any], groups: list[str], dyn_client: DynamicClient +) -> str | None: + # Extract pod metadata + try: + pod_metadata = pod.get('metadata', {}) + pod_labels = pod_metadata.get('labels', {}) + pod_user = pod_labels.get('opendatahub.io/user', None) + except AttributeError as e: + LOG.error(f'Error extracting pod information: {e}') + return None + + if pod_user is None: + return None + + pod_user = decode_pod_user(pod_user) + + group_resource = get_group_resource(dyn_client) + + # Iterate through classes + for group in groups: + users = get_group_members(group_resource, group) + + # Check if group has no users + if not users: + LOG.warning(f'Group {group} has no users or users attribute is not a list.') + continue + + # Compare users in the groups (classes) with the pod user + if pod_user in users: + LOG.info(f'Assigning class label: {group} to user {pod_user}') + return group + + return None + + +def create_app(**config: Any) -> Flask: + app = Flask(__name__) + app.config.from_prefixed_env('RHOAI_CLASS') + app.config.update(config) + + if not app.config['GROUPS']: + LOG.error('RHOAI_CLASS_GROUPS environment variables are required.') + exit(1) + + groups = app.config['GROUPS'].split(',') + + dyn_client = get_client() + + @app.route('/mutate', methods=['POST']) + def mutate_pod() -> Response: + # Grab pod for mutation and validate request + try: + admission_review = AdmissionReview(**request.get_json()) + except ValidationError as e: + LOG.error('Validation error: %s', e) + return ( + jsonify( + AdmissionReviewResponse( + response=AdmissionResponse( + uid=request.json.get('request', {}).get('uid', ''), + allowed=False, + status=Status(message=f'Invalid request: {e}'), + ) + ).model_dump() + ), + 400, + {'content-type': 'application/json'}, + ) + + uid = admission_review.request.uid + pod = admission_review.request.object.model_dump() + + # Grab class that the pod user belongs to + try: + class_label = assign_class_label(pod, groups, dyn_client) + except Exception as err: + LOG.error('failed to assign class label: %s', err) + return 'unexpected error encountered', 500, {'content-type': 'text/plain'} + + # If user not in any class, return without modifications + if not class_label: + return ( + jsonify( + AdmissionReviewResponse( + response=AdmissionResponse( + uid=uid, + allowed=True, + status=Status(message='No class label assigned.'), + ) + ).model_dump() + ), + 200, + {'content-type': 'application/json'}, + ) + + # Generate JSON Patch to add class label + patch = [ + { + 'op': 'add', + 'path': '/metadata/labels/nerc.mghpcc.org~1class', + 'value': class_label, + } + ] + + # Encode patch as base64 for response + patch_base64 = base64.b64encode(json.dumps(patch).encode('utf-8')).decode( + 'utf-8' + ) + + # Return webhook response that includes the patch to add class label + return ( + jsonify( + AdmissionReviewResponse( + response=AdmissionResponse( + uid=uid, allowed=True, patchType='JSONPatch', patch=patch_base64 + ) + ).model_dump() + ), + 200, + {'content-type': 'application/json'}, + ) + + return app diff --git a/container-images/assign-class-label/requirements.txt b/container-images/assign-class-label/requirements.txt new file mode 100644 index 0000000..fde8659 --- /dev/null +++ b/container-images/assign-class-label/requirements.txt @@ -0,0 +1,5 @@ +flask +kubernetes +openshift +gunicorn +pydantic diff --git a/container-images/assign-class-label/test-requirements.txt b/container-images/assign-class-label/test-requirements.txt new file mode 100644 index 0000000..71cc539 --- /dev/null +++ b/container-images/assign-class-label/test-requirements.txt @@ -0,0 +1,2 @@ +pytest +ruff diff --git a/container-images/assign-class-label/tests/test_mutate.py b/container-images/assign-class-label/tests/test_mutate.py new file mode 100644 index 0000000..6386333 --- /dev/null +++ b/container-images/assign-class-label/tests/test_mutate.py @@ -0,0 +1,243 @@ +import pytest +import mutate + +from unittest import mock + + +@pytest.fixture() +def app(): + with mock.patch('mutate.get_client') as mock_get_client, mock.patch( + 'mutate.get_group_resource' + ) as mock_get_group_resource, mock.patch( + 'mutate.get_group_members' + ) as mock_group_members: + mock_object = mock.Mock() + mock_get_client.return_value = mock_object + mock_get_group_resource.return_value = mock_object + + def mock_get_group_members(group_resource, group_name): + if group_name == 'class1': + return ['user1'] + elif group_name == 'class2': + return ['user2'] + return [] + + mock_group_members.side_effect = mock_get_group_members + + app = mutate.create_app(GROUPS='class1,class2', LABEL='testlabel') + + app.config.update( + { + 'TESTING': True, + } + ) + + yield app + + +@pytest.fixture() +def client(app): + return app.test_client() + + +# Sending empty json request +def test_mutate_bad_data(client): + res = client.post('/mutate', json={}) + + assert res.status_code == 400 + + res_json = res.get_json() + + assert 'Invalid request' in res_json['response']['status']['message'] + + +# Calling a bad path +def test_bad_path(client): + res = client.get('/fake_path') + assert res.status_code == 404 + + assert 'The requested URL was not found' in res.data.decode('utf-8') + + +# Proper request that contains no metadata +def test_request_no_metadata(client): + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '1234', + 'object': {'metadata': {}}, + } + }, + ) + + assert res.status_code == 200 + assert res.json == { + 'apiVersion': 'admission.k8s.io/v1', + 'kind': 'AdmissionReview', + 'response': { + 'allowed': True, + 'status': {'message': 'No class label assigned.'}, + 'uid': '1234', + 'patchType': None, + 'patch': None, + }, + } + + +# Exeption response +def test_api_exception(client): + mutate.get_group_members.side_effect = ValueError('this is a test') + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '1234', + 'object': { + 'metadata': { + 'labels': { + 'opendatahub.io/user': 'testuser1', + } + } + }, + } + }, + ) + assert res.status_code == 500 + assert res.text == 'unexpected error encountered' + + +# If user1 in group1, returns successful json patch +def test_valid_request(client): + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '1234', + 'object': { + 'metadata': { + 'labels': { + 'opendatahub.io/user': 'user1', + } + } + }, + } + }, + ) + + print(f'Response code: {res.status_code}') + print(f'Response JSON: {res.get_json()}') + assert res.status_code == 200 + assert res.get_json()['response']['patchType'] == 'JSONPatch' + + +# Pod has invalid (empty) UID +def test_invalid_uid(client): + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '', + 'object': { + 'metadata': { + 'labels': { + 'opendatahub.io/user': 'user1', + } + } + }, + } + }, + ) + + assert res.status_code == 400 + + assert 'Invalid request' in res.get_json()['response']['status']['message'] + + +# Request formatted correctly but contains empty or no user +def test_no_user(client): + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '1234', + 'object': { + 'metadata': { + 'labels': { + 'opendatahub.io/user': '', + } + } + }, + } + }, + ) + + assert res.status_code == 200 + assert res.json == { + 'apiVersion': 'admission.k8s.io/v1', + 'kind': 'AdmissionReview', + 'response': { + 'allowed': True, + 'status': {'message': 'No class label assigned.'}, + 'uid': '1234', + 'patchType': None, + 'patch': None, + }, + } + + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '1234', + 'object': {'metadata': {'labels': {}}}, + } + }, + ) + + assert res.status_code == 200 + assert res.json == { + 'apiVersion': 'admission.k8s.io/v1', + 'kind': 'AdmissionReview', + 'response': { + 'allowed': True, + 'status': {'message': 'No class label assigned.'}, + 'uid': '1234', + 'patchType': None, + 'patch': None, + }, + } + + +# Valid request but all groups empty +def test_empty_group_members(client): + with mock.patch('mutate.get_group_members') as mock_group_members: + mock_group_members.return_value = [] + res = client.post( + '/mutate', + json={ + 'request': { + 'uid': '1234', + 'object': { + 'metadata': { + 'labels': { + 'opendatahub.io/user': 'user1', + } + } + }, + } + }, + ) + + assert res.status_code == 200 + assert res.json == { + 'apiVersion': 'admission.k8s.io/v1', + 'kind': 'AdmissionReview', + 'response': { + 'allowed': True, + 'status': {'message': 'No class label assigned.'}, + 'uid': '1234', + 'patchType': None, + 'patch': None, + }, + } diff --git a/container-images/assign-class-label/wsgi.py b/container-images/assign-class-label/wsgi.py new file mode 100644 index 0000000..03851b3 --- /dev/null +++ b/container-images/assign-class-label/wsgi.py @@ -0,0 +1,6 @@ +from mutate import create_app + +webhook = create_app() + +if __name__ == '__main__': + webhook.run() diff --git a/docker/Dockerfile b/container-images/group-sync/Dockerfile similarity index 100% rename from docker/Dockerfile rename to container-images/group-sync/Dockerfile diff --git a/docker/src/python/group-sync/group-sync.py b/container-images/group-sync/group-sync.py similarity index 57% rename from docker/src/python/group-sync/group-sync.py rename to container-images/group-sync/group-sync.py index d1b1c93..d8ab1a9 100644 --- a/docker/src/python/group-sync/group-sync.py +++ b/container-images/group-sync/group-sync.py @@ -8,10 +8,10 @@ def add_users_to_group(group): # Run the `oc get` command, capture the JSON output, and load the data - rolebinding = oc.selector("rolebinding/edit").object() + rolebinding = oc.selector('rolebinding/edit').object() users_in_rolebinding = set( - subject["name"] for subject in rolebinding.model.subjects + subject['name'] for subject in rolebinding.model.subjects ) users_in_group = set(group.model.users) @@ -19,28 +19,28 @@ def add_users_to_group(group): users_to_remove = users_in_group.difference(users_in_rolebinding) group_name = group.model.metadata.name - LOG.info("adding to group %s: %s", group_name, users_to_add) - LOG.info("removing from group %s: %s", group_name, users_to_remove) - group.patch({"users": list(users_in_rolebinding)}) + LOG.info('adding to group %s: %s', group_name, users_to_add) + LOG.info('removing from group %s: %s', group_name, users_to_remove) + group.patch({'users': list(users_in_rolebinding)}) -if __name__ == "__main__": +if __name__ == '__main__': # Use environment variables for group name and namespace - group_name = os.environ.get("GROUP_NAME") - namespace_name = os.environ.get("NAMESPACE") + group_name = os.environ.get('GROUP_NAME') + namespace_name = os.environ.get('NAMESPACE') - logging.basicConfig(level="INFO") + logging.basicConfig(level='INFO') # Check if the required environment variables are present if not group_name or not namespace_name: - LOG.error("GROUP_NAME and NAMESPACE environment variables are required.") + LOG.error('GROUP_NAME and NAMESPACE environment variables are required.') sys.exit(1) # Check that group exists try: - group = oc.selector(f"group/{group_name}").object() + group = oc.selector(f'group/{group_name}').object() except oc.model.OpenShiftPythonException: - LOG.error("Unable to find group %s", group_name) + LOG.error('Unable to find group %s', group_name) sys.exit(1) # Run add_users_to_group in the given namespace diff --git a/docker/src/python/group-sync/requirements.txt b/container-images/group-sync/requirements.txt similarity index 100% rename from docker/src/python/group-sync/requirements.txt rename to container-images/group-sync/requirements.txt diff --git a/scripts/get_url.py b/scripts/get_url.py index 95895a3..87745a4 100644 --- a/scripts/get_url.py +++ b/scripts/get_url.py @@ -1,22 +1,31 @@ import subprocess -import os import yaml + def extract_url(notebook_name): - cmd = ["oc", "get", "notebook", notebook_name, "-o", "yaml", "-n", "rhods-notebooks"] + cmd = [ + 'oc', + 'get', + 'notebook', + notebook_name, + '-o', + 'yaml', + '-n', + 'rhods-notebooks', + ] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: - print(f"Error getting YAML for notebook {notebook_name}:", result.stderr) + print(f'Error getting YAML for notebook {notebook_name}:', result.stderr) return None data = yaml.safe_load(result.stdout) url = data.get('metadata', {}).get('annotations', {}).get('opendatahub.io/link') return url -notebook_name = input("Enter the notebook name: ") + +notebook_name = input('Enter the notebook name: ') url = extract_url(notebook_name) if url: - print(f"URL for notebook {notebook_name}: {url}") + print(f'URL for notebook {notebook_name}: {url}') else: - print(f"No URL found for notebook {notebook_name}") - + print(f'No URL found for notebook {notebook_name}') diff --git a/webhooks/assign-class-label/deployment.yaml b/webhooks/assign-class-label/deployment.yaml new file mode 100644 index 0000000..6cb1c3d --- /dev/null +++ b/webhooks/assign-class-label/deployment.yaml @@ -0,0 +1,31 @@ +kind: Deployment +apiVersion: apps/v1 +metadata: + name: assign-class-label-webhook +spec: + replicas: 1 + template: + spec: + containers: + - name: assign-class-label + image: quay.io/rh-ee-istaplet/ope-webhooks:assign-class-label-webhook + imagePullPolicy: Always + ports: + - containerPort: 443 + volumeMounts: + - name: cert + mountPath: /certs + readOnly: true + resources: + limits: + cpu: 500m + memory: 512Mi + env: + # EDIT VALUE HERE BEFORE RUNNING, must be comma separated + - name: RHOAI_CLASS_GROUPS + value: "cs210,cs506,ee440" + serviceAccountName: webhook-sa + volumes: + - name: cert + secret: + secretName: webhook-cert diff --git a/webhooks/assign-class-label/kustomization.yaml b/webhooks/assign-class-label/kustomization.yaml new file mode 100644 index 0000000..5bcb16d --- /dev/null +++ b/webhooks/assign-class-label/kustomization.yaml @@ -0,0 +1,13 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: rhods-notebooks +commonLabels: + app: assign-class-label-webhook + +resources: + - deployment.yaml + - service.yaml + - webhook-config.yaml + - serviceaccount.yaml + - role.yaml + - rolebinding.yaml diff --git a/webhooks/assign-class-label/role.yaml b/webhooks/assign-class-label/role.yaml new file mode 100644 index 0000000..5640af6 --- /dev/null +++ b/webhooks/assign-class-label/role.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: ope-webhook-role +rules: +- apiGroups: ["user.openshift.io"] + resources: ["pods", "groups"] + verbs: ["get", "list", "watch", "patch"] diff --git a/webhooks/assign-class-label/rolebinding.yaml b/webhooks/assign-class-label/rolebinding.yaml new file mode 100644 index 0000000..48cebf2 --- /dev/null +++ b/webhooks/assign-class-label/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: ope-webhook-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: ope-webhook-role +subjects: +- kind: ServiceAccount + name: webhook-sa + namespace: rhods-notebooks diff --git a/webhooks/assign-class-label/service.yaml b/webhooks/assign-class-label/service.yaml new file mode 100644 index 0000000..06bd0d1 --- /dev/null +++ b/webhooks/assign-class-label/service.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Service +metadata: + name: assign-class-label-webhook +spec: + ports: + - name: https + protocol: TCP + port: 443 + targetPort: 5000 diff --git a/webhooks/assign-class-label/serviceaccount.yaml b/webhooks/assign-class-label/serviceaccount.yaml new file mode 100644 index 0000000..86983aa --- /dev/null +++ b/webhooks/assign-class-label/serviceaccount.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: webhook-sa + namespace: rhods-notebooks diff --git a/webhooks/assign-class-label/webhook-config.yaml b/webhooks/assign-class-label/webhook-config.yaml new file mode 100644 index 0000000..e7fef76 --- /dev/null +++ b/webhooks/assign-class-label/webhook-config.yaml @@ -0,0 +1,25 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: assign-class-label-webhook +webhooks: +- name: assign-class-label-webhook.nerc.com + clientConfig: + service: + namespace: rhods-notebooks + name: assign-class-label-webhook + path: /mutate + caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURvakNDQW9xZ0F3SUJBZ0lVTlpUUGJOMVNBdnNzVUdUTHRUQW1FamE5TmJzd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1FqRUxNQWtHQTFVRUJoTUNXRmd4RlRBVEJnTlZCQWNNREVSbFptRjFiSFFnUTJsMGVURWNNQm9HQTFVRQpDZ3dUUkdWbVlYVnNkQ0JEYjIxd1lXNTVJRXgwWkRBZUZ3MHlOREE1TVRjeE5ESXlNekZhRncweU56QTNNRGd4Ck5ESXlNekZhTUVJeEN6QUpCZ05WQkFZVEFsaFlNUlV3RXdZRFZRUUhEQXhFWldaaGRXeDBJRU5wZEhreEhEQWEKQmdOVkJBb01FMFJsWm1GMWJIUWdRMjl0Y0dGdWVTQk1kR1F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQgpEd0F3Z2dFS0FvSUJBUUN3Z2RyWGVSek1BYW1TT1hwYnB1a0dsdTdBL1lpVTl1bytZbTI1Zm9aRmhrQXBJeWY1CkNCNzIzRW0wbUtrdmdtR0VjYy9oZ1kwYUxnV3dHZTBGRW40ZjZGQ21RRjJzMlRHMEJPUDZ4Z3NjUHlGMU41NmgKKzRwVnE4UXFrbTA3YktxcEhZSzF2czVTd2tNTXExYjBZWjdBd0Rjd3p6VWtWM20zb0VtSnptR2U1L1JUd1puTApGdlg0LzRsUEdkMGF6MnBlUnBudllOOWZLQWd3bERxMklFdjJJZUljTWpOMmxVQTAxYkhGc09qZXAyakU1dWQrCi9DWmwxZHRuWkJYWWY1N01DNFc2MitmZnlRWExyOGt1clNUN0Z5Zi9OTjVRblhxNDBQSHd4N3dWOHlzY3l6M2sKWmRsalBiTHBMQTJYeE5ZcXJRMDBid1FDVS9kVTVPZ0VaZVU3QWdNQkFBR2pnWTh3Z1l3d0hRWURWUjBPQkJZRQpGUDBobG4xTGs0aXcwUTNwTnVUNVNsWHNKdDYvTUI4R0ExVWRJd1FZTUJhQUZQMGhsbjFMazRpdzBRM3BOdVQ1ClNsWHNKdDYvTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3T1FZRFZSMFJCREl3TUlJdVlYTnphV2R1TFdOc1lYTnoKTFd4aFltVnNMWGRsWW1odmIyc3VjbWh2WkhNdGJtOTBaV0p2YjJ0ekxuTjJZekFOQmdrcWhraUc5dzBCQVFzRgpBQU9DQVFFQVNSdWZpcDI3TmFQVC8wZGZ5VU85MDMxV2F6Z3U0YzY4dG1FVXlBYXpvNlY2Z1lzK005YnVERDNJCkl3ZkkvUXB5bGhIb01EZ3FjcjlyZ1NHY2lvbTFocXM4RjAyNGFaa3BobzV3LzRiSUJ2VWVUVHVKNlhyYzhJOCsKSW1ESHh1SFREVXZ4bG15U0NWc0lOSlBUZWprQ0RQRVR0bVlzaHlNZkxyM1NOYUZSYXU4VlNqUFNsd2VDNVkzUQo4Q3I0Q2V0eHlCdlp0enhpTXZvemR0dGx3d3RGejRaMmQ5ZmRIWmZWbms5UFlqZFpRb3o1cVlBbklkcGlJemo3Ci92UUxQZ1RpbmRDWkIvTW1PbHRBZ0UrNmQrQ2lsOW1yOHlRVVlTdlYzL015SUU1ZDd5M0h0VnJrRnhKYkZqNjEKVjVpOUR1YWNpTXpMUVM2UzJOUUJkcFpINzc0NHZ3PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + rules: + - operations: ["CREATE"] + apiGroups: [""] + apiVersions: ["v1"] + resources: ["pods"] + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: In + values: + - rhods-notebooks + sideEffects: None + admissionReviewVersions: ["v1"]