├── CODEOWNERS ├── .gitignore ├── renovate.json ├── config.yaml ├── terraform ├── versions.tf ├── outputs.tf ├── main.tf ├── variables.tf └── README.md ├── tests ├── unit │ ├── __init__.py │ └── test_operator.py └── integration │ ├── poddefault_test_workloads.yaml │ └── test_charm.py ├── tools ├── get-images.sh └── get-images-1.7-stable.sh ├── README.md ├── .github ├── workflows │ ├── on_pull_request_lint_title.yaml │ ├── on_pull_request.yaml │ ├── on_push.yaml │ ├── release.yaml │ ├── get-charm-paths.sh │ ├── tiobe_scan.yaml │ ├── integrate.yaml │ └── publish.yaml ├── .jira_sync_config.yaml └── ISSUE_TEMPLATE │ ├── task.yaml │ └── bug.yaml ├── concierge.yaml ├── src ├── templates │ ├── webhook_configuration.yaml.j2 │ ├── ssl.conf.j2 │ ├── auth_manifests.yaml.j2 │ └── crds.yaml.j2 ├── certs.py └── charm.py ├── metadata.yaml ├── tox.ini ├── pyproject.toml ├── icon.svg ├── CONTRIBUTING.md ├── charmcraft.yaml ├── LICENSE └── lib └── charms └── observability_libs └── v1 └── kubernetes_service_patch.py /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @canonical/kubeflow 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | *.charm 3 | build/ 4 | .tox/ 5 | __pycache__ 6 | .idea 7 | venv/ 8 | .terraform* 9 | *.tfstate* 10 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>canonical/charmed-kubeflow-workflows" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | options: 4 | port: 5 | type: int 6 | default: 4443 7 | description: Webhook server port 8 | -------------------------------------------------------------------------------- /terraform/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6" 3 | required_providers { 4 | juju = { 5 | source = "juju/juju" 6 | version = ">= 0.14.0, < 1.0.0" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Initialize unit tests 3 | # 4 | 5 | """Setup test environment for unit tests.""" 6 | 7 | import ops.testing 8 | 9 | # enable simulation of container networking 10 | ops.testing.SIMULATE_CAN_CONNECT = True 11 | -------------------------------------------------------------------------------- /terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "app_name" { 2 | value = juju_application.admission_webhook.name 3 | } 4 | 5 | output "provides" { 6 | value = { 7 | pod_defaults = "pod-defaults", 8 | } 9 | } 10 | 11 | output "requires" { 12 | value = { 13 | "logging" = "logging", 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tools/get-images.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script returns list of container images that are managed by this charm and/or its workload 4 | # 5 | # dynamic list 6 | IMAGE_LIST=() 7 | IMAGE_LIST+=($(find -type f -name metadata.yaml -exec yq '.resources | to_entries | .[] | .value | ."upstream-source"' {} \;)) 8 | printf "%s\n" "${IMAGE_LIST[@]}" 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Admission Webhook Operator 2 | 3 | ### Overview 4 | This charm encompasses the Kubernetes Python operator for Kubeflow's Admission Webhook (see 5 | [CharmHub](https://charmhub.io/?q=admission-webhook)). 6 | 7 | ## Install 8 | 9 | To install the Admission Webhook, run: 10 | 11 | juju deploy admission-webhook 12 | 13 | For more information, see https://juju.is/docs 14 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | resource "juju_application" "admission_webhook" { 2 | charm { 3 | name = "admission-webhook" 4 | base = var.base 5 | channel = var.channel 6 | revision = var.revision 7 | } 8 | config = var.config 9 | model = var.model_name 10 | name = var.app_name 11 | resources = var.resources 12 | trust = true 13 | units = 1 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/on_pull_request_lint_title.yaml: -------------------------------------------------------------------------------- 1 | name: Lint PR title 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - reopened 9 | - synchronize 10 | 11 | permissions: 12 | pull-requests: read 13 | 14 | jobs: 15 | lint: 16 | uses: canonical/charmed-kubeflow-workflows/.github/workflows/_pull-request-lint-title.yaml@main 17 | secrets: inherit 18 | -------------------------------------------------------------------------------- /tools/get-images-1.7-stable.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script returns list of container images that are managed by this charm and/or its workload 4 | # 5 | # static list 6 | STATIC_IMAGE_LIST=( 7 | ) 8 | # dynamic list 9 | git checkout origin/track/1.7 10 | IMAGE_LIST=() 11 | IMAGE_LIST+=($(find -type f -name metadata.yaml -exec yq '.resources | to_entries | .[] | .value | ."upstream-source"' {} \;)) 12 | 13 | printf "%s\n" "${STATIC_IMAGE_LIST[@]}" 14 | printf "%s\n" "${IMAGE_LIST[@]}" 15 | -------------------------------------------------------------------------------- /concierge.yaml: -------------------------------------------------------------------------------- 1 | juju: 2 | channel: 3.6/stable 3 | model-defaults: 4 | logging-config: =INFO; unit=DEBUG 5 | 6 | providers: 7 | k8s: 8 | enable: true 9 | bootstrap: true 10 | channel: 1.32-classic/stable 11 | features: 12 | local-storage: 13 | enabled: true 14 | bootstrap-constraints: 15 | root-disk: 2G 16 | 17 | lxd: 18 | enable: true 19 | bootstrap: false 20 | channel: latest/stable 21 | 22 | host: 23 | snaps: 24 | charmcraft: 25 | channel: 3.x/stable 26 | -------------------------------------------------------------------------------- /.github/workflows/on_pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: On Pull Request 2 | 3 | # On pull_request, we: 4 | # * always publish to charmhub at latest/edge/branchname 5 | # * always run tests 6 | 7 | on: 8 | pull_request: 9 | 10 | jobs: 11 | 12 | tests: 13 | name: Run Tests 14 | uses: ./.github/workflows/integrate.yaml 15 | secrets: inherit 16 | 17 | # publish runs in parallel with tests, as we always publish in this situation 18 | publish-charm: 19 | name: Publish Charm 20 | uses: ./.github/workflows/publish.yaml 21 | secrets: inherit 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/on_push.yaml: -------------------------------------------------------------------------------- 1 | name: On Push 2 | 3 | # On push to a "special" branch, we: 4 | # * always publish to charmhub at latest/edge/branchname 5 | # * always run tests 6 | # where a "special" branch is one of main or track/**, as 7 | # by convention these branches are the source for a corresponding 8 | # charmhub edge channel. 9 | 10 | on: 11 | push: 12 | branches: 13 | - main 14 | - track/** 15 | 16 | jobs: 17 | 18 | tests: 19 | name: Run Tests 20 | uses: ./.github/workflows/integrate.yaml 21 | secrets: inherit 22 | 23 | # publish runs in series with tests, and only publishes if tests passes 24 | publish-charm: 25 | name: Publish Charm 26 | needs: tests 27 | uses: ./.github/workflows/publish.yaml 28 | secrets: inherit 29 | 30 | -------------------------------------------------------------------------------- /src/templates/webhook_configuration.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: {{ app_name }} 5 | webhooks: 6 | - admissionReviewVersions: 7 | - v1beta1 8 | - v1 9 | clientConfig: 10 | caBundle: {{ ca_bundle }} 11 | service: 12 | name: {{ service_name }} 13 | namespace: {{ namespace }} 14 | path: /apply-poddefault 15 | port: {{ port }} 16 | sideEffects: None 17 | failurePolicy: Fail 18 | name: admission-webhook.kubeflow.org 19 | namespaceSelector: 20 | matchLabels: 21 | app.kubernetes.io/part-of: kubeflow-profile 22 | rules: 23 | - apiGroups: 24 | - "" 25 | apiVersions: 26 | - v1 27 | operations: 28 | - CREATE 29 | resources: 30 | - pods 31 | -------------------------------------------------------------------------------- /src/templates/ssl.conf.j2: -------------------------------------------------------------------------------- 1 | [ req ] 2 | default_bits = 2048 3 | prompt = no 4 | default_md = sha256 5 | req_extensions = req_ext 6 | distinguished_name = dn 7 | [ dn ] 8 | C = GB 9 | ST = Canonical 10 | L = Canonical 11 | O = Canonical 12 | OU = Canonical 13 | CN = 127.0.0.1 14 | [ req_ext ] 15 | subjectAltName = @alt_names 16 | [ alt_names ] 17 | DNS.1 = {{ service_name }} 18 | DNS.2 = {{ service_name }}.{{ model }} 19 | DNS.3 = {{ service_name }}.{{ model }}.svc 20 | DNS.4 = {{ service_name }}.{{ model }}.svc.cluster 21 | DNS.5 = {{ service_name }}.{{ model }}.svc.cluster.local 22 | IP.1 = 127.0.0.1 23 | [ v3_ext ] 24 | authorityKeyIdentifier=keyid,issuer:always 25 | basicConstraints=CA:FALSE 26 | keyUsage=keyEncipherment,dataEncipherment,digitalSignature 27 | extendedKeyUsage=serverAuth,clientAuth 28 | subjectAltName=@alt_names 29 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "app_name" { 2 | description = "Application name" 3 | type = string 4 | default = "admission-webhook" 5 | } 6 | 7 | variable "base" { 8 | description = "Charm base" 9 | type = string 10 | default = "ubuntu@24.04" 11 | } 12 | 13 | variable "channel" { 14 | description = "Charm channel" 15 | type = string 16 | default = "latest/edge" 17 | } 18 | 19 | variable "config" { 20 | description = "Map of charm configuration options" 21 | type = map(string) 22 | default = {} 23 | } 24 | 25 | variable "model_name" { 26 | description = "Model name" 27 | type = string 28 | } 29 | 30 | variable "resources" { 31 | description = "Map of resources" 32 | type = map(string) 33 | default = null 34 | } 35 | 36 | variable "revision" { 37 | description = "Charm revision" 38 | type = number 39 | default = null 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # reusable workflow triggered manually 2 | name: Release charm to other tracks and channels 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | destination-channel: 8 | description: 'Destination Channel' 9 | required: true 10 | origin-channel: 11 | description: 'Origin Channel' 12 | required: true 13 | 14 | jobs: 15 | promote-charm: 16 | name: Promote charm 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Release charm to channel 21 | uses: canonical/charming-actions/release-charm@2.6.2 22 | with: 23 | credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} 24 | github-token: ${{ secrets.GITHUB_TOKEN }} 25 | destination-channel: ${{ github.event.inputs.destination-channel }} 26 | origin-channel: ${{ github.event.inputs.origin-channel }} 27 | base-channel: "24.04" 28 | -------------------------------------------------------------------------------- /.github/workflows/get-charm-paths.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -x 2 | 3 | # Finds the charms in this repo, outputting them as JSON 4 | # Will return one of: 5 | # * the relative paths of the directories listed in `./charms`, if that directory exists 6 | # * "./", if the root directory has a "metadata.yaml" file 7 | # * otherwise, error 8 | # 9 | # Modified from: https://stackoverflow.com/questions/63517732/github-actions-build-matrix-for-lambda-functions/63736071#63736071 10 | CHARMS_DIR="./charms" 11 | if [ -d "$CHARMS_DIR" ]; 12 | then 13 | CHARM_PATHS=$(find $CHARMS_DIR -maxdepth 1 -type d -not -path '*/\.*' -not -path "$CHARMS_DIR") 14 | else 15 | if [ -f "./metadata.yaml" ] 16 | then 17 | CHARM_PATHS="./" 18 | else 19 | echo "Cannot find valid charm directories - aborting" 20 | exit 1 21 | fi 22 | fi 23 | 24 | # Convert output to JSON string format 25 | # { charm_paths: [...] } 26 | CHARM_PATHS_LIST=$(echo "$CHARM_PATHS" | jq -c --slurp --raw-input 'split("\n")[:-1]') 27 | 28 | echo "Found CHARM_PATHS_LIST: $CHARM_PATHS_LIST" 29 | 30 | echo "::set-output name=CHARM_PATHS_LIST::$CHARM_PATHS_LIST" 31 | -------------------------------------------------------------------------------- /.github/.jira_sync_config.yaml: -------------------------------------------------------------------------------- 1 | settings: 2 | # Jira project key to create the issue in 3 | jira_project_key: "KF" 4 | 5 | # Dictionary mapping GitHub issue status to Jira issue status 6 | status_mapping: 7 | opened: Untriaged 8 | closed: done 9 | 10 | # (Optional) GitHub labels. Only issues with one of those labels will be synchronized. 11 | # If not specified, all issues will be synchronized 12 | labels: 13 | - bug 14 | - enhancement 15 | 16 | # (Optional) (Default: false) Add a new comment in GitHub with a link to Jira created issue 17 | add_gh_comment: true 18 | 19 | # (Optional) (Default: true) Synchronize issue description from GitHub to Jira 20 | sync_description: true 21 | 22 | # (Optional) (Default: true) Synchronize comments from GitHub to Jira 23 | sync_comments: false 24 | 25 | # (Optional) (Default: None) Parent Epic key to link the issue to 26 | epic_key: "KF-4805" 27 | 28 | # (Optional) Dictionary mapping GitHub issue labels to Jira issue types. 29 | # If label on the issue is not in specified list, this issue will be created as a Bug 30 | label_mapping: 31 | enhancement: Story 32 | -------------------------------------------------------------------------------- /metadata.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | name: admission-webhook 4 | summary: Injects common data (e.g. env vars, volumes) to pods (e.g. notebooks) 5 | description: | 6 | https://github.com/kubeflow/kubeflow/tree/master/components/admission-webhook 7 | website: https://charmhub.io/admission-webhook 8 | source: https://github.com/canonical/admission-webhook-operator 9 | issues: https://github.com/canonical/admission-webhook-index/issues 10 | docs: https://discourse.charmhub.io/t/admission-webhook-index/8210 11 | containers: 12 | admission-webhook: 13 | resource: oci-image 14 | uid: 584792 15 | gid: 584792 16 | mounts: 17 | - storage: certs 18 | location: /etc/webhook/certs 19 | resources: 20 | oci-image: 21 | type: oci-image 22 | description: Backing OCI image 23 | auto-fetch: true 24 | upstream-source: charmedkubeflow/admission-webhook:1.10.0-8dd1032 25 | provides: 26 | pod-defaults: 27 | interface: pod-defaults 28 | requires: 29 | logging: 30 | interface: loki_push_api 31 | optional: true 32 | charm-user: non-root 33 | storage: 34 | certs: 35 | type: filesystem 36 | minimum-size: 1M 37 | -------------------------------------------------------------------------------- /.github/workflows/tiobe_scan.yaml: -------------------------------------------------------------------------------- 1 | name: TICS run self-hosted test (github-action) 2 | 3 | on: 4 | workflow_dispatch: # Allows manual triggering 5 | schedule: 6 | - cron: "0 2 * * 6" # Every Saturday 2:00 AM UTC 7 | 8 | jobs: 9 | build: 10 | runs-on: [self-hosted, linux, amd64, tiobe, jammy] 11 | 12 | steps: 13 | - name: Checkout the project 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up Python 3.12 17 | uses: actions/setup-python@v5.6.0 18 | with: 19 | python-version: "3.12" 20 | 21 | - name: Install dependencies 22 | run: | 23 | pip install tox 24 | pip install pylint flake8 25 | 26 | - name: Run tox tests to create coverage.xml 27 | run: tox run -e unit 28 | 29 | - name: move results to necessary folder for TICS 30 | run: | 31 | mkdir cover 32 | mv coverage.xml cover/coverage.xml 33 | 34 | - name: Run TICS analysis with github-action 35 | uses: tiobe/tics-github-action@v3 36 | with: 37 | mode: qserver 38 | project: admission-webhook-operator 39 | branchdir: . 40 | viewerUrl: https://canonical.tiobe.com/tiobeweb/TICS/api/cfg?name=default 41 | ticsAuthToken: ${{ secrets.TICSAUTHTOKEN }} 42 | installTics: true 43 | -------------------------------------------------------------------------------- /tests/integration/poddefault_test_workloads.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: test-admission-webhook-user-namespace 5 | labels: 6 | app.kubernetes.io/part-of: kubeflow-profile 7 | --- 8 | apiVersion: kubeflow.org/v1alpha1 9 | kind: PodDefault 10 | metadata: 11 | name: access-ml-pipeline 12 | namespace: test-admission-webhook-user-namespace 13 | spec: 14 | desc: Allow access to Kubeflow Pipelines 15 | selector: 16 | matchLabels: 17 | access-ml-pipeline: "true" 18 | volumes: 19 | - name: volume-kf-pipeline-token 20 | projected: 21 | sources: 22 | - serviceAccountToken: 23 | path: token 24 | expirationSeconds: 7200 25 | audience: pipelines.kubeflow.org 26 | volumeMounts: 27 | - mountPath: /var/run/secrets/kubeflow/pipelines 28 | name: volume-kf-pipeline-token 29 | readOnly: true 30 | env: 31 | - name: KF_PIPELINES_SA_TOKEN_PATH 32 | value: /var/run/secrets/kubeflow/pipelines/token 33 | --- 34 | apiVersion: v1 35 | kind: Pod 36 | metadata: 37 | labels: 38 | access-ml-pipeline: "true" 39 | name: testpod 40 | namespace: test-admission-webhook-user-namespace 41 | spec: 42 | containers: 43 | - args: 44 | - "while true; do sleep 3600; done" 45 | command: ["/bin/bash", "-c", "--"] 46 | image: ubuntu:latest 47 | imagePullPolicy: Always 48 | name: ubuntu -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.yaml: -------------------------------------------------------------------------------- 1 | name: Task 2 | description: File an enhancement proposal 3 | labels: "enhancement" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out this enhancement 9 | proposal! Before submitting your issue, please make sure there 10 | isn't already a prior issue concerning this. If there is, 11 | please join that discussion instead. 12 | - type: textarea 13 | id: enhancement-proposal-context 14 | attributes: 15 | label: Context 16 | description: > 17 | Describe why we should work on this task/enhancement, as well as 18 | existing context we should be aware of 19 | validations: 20 | required: true 21 | - type: textarea 22 | id: enhancement-proposal-what 23 | attributes: 24 | label: What needs to get done 25 | description: > 26 | Describe what needs to get done 27 | placeholder: | 28 | 1. Look into X 29 | 2. Implement Y 30 | 3. Create file Z 31 | validations: 32 | required: true 33 | - type: textarea 34 | id: enhancement-proposal-dod 35 | attributes: 36 | label: Definition of Done 37 | description: > 38 | What are the requirements for the task to be considered done 39 | placeholder: | 40 | 1. We know how X works (spike) 41 | 2. Code is doing Y 42 | 3. Charm has functionality Z 43 | validations: 44 | required: true 45 | -------------------------------------------------------------------------------- /src/templates/auth_manifests.yaml.j2: -------------------------------------------------------------------------------- 1 | # Source: kubeflow/manifests/apps/admission-webhook/upstream/base/cluster-role.yaml 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ app_name }} 6 | rules: 7 | - apiGroups: 8 | - kubeflow.org 9 | resources: 10 | - poddefaults 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - update 16 | - create 17 | - patch 18 | - delete 19 | --- 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | kind: ClusterRoleBinding 22 | metadata: 23 | name: {{ app_name }} 24 | roleRef: 25 | apiGroup: rbac.authorization.k8s.io 26 | kind: ClusterRole 27 | name: {{ app_name }} 28 | subjects: 29 | - kind: ServiceAccount 30 | name: {{ app_name }} 31 | namespace: {{ namespace }} 32 | --- 33 | aggregationRule: 34 | clusterRoleSelectors: 35 | - matchLabels: 36 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-poddefaults-admin: "true" 37 | apiVersion: rbac.authorization.k8s.io/v1 38 | kind: ClusterRole 39 | metadata: 40 | labels: 41 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-admin: "true" 42 | name: admission-webhook-kubeflow-poddefaults-admin 43 | # Commenting out rules: [] due to https://github.com/gtsystem/lightkube/issues/32 44 | #rules: [] 45 | --- 46 | aggregationRule: 47 | clusterRoleSelectors: 48 | - matchLabels: 49 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-poddefaults-edit: "true" 50 | apiVersion: rbac.authorization.k8s.io/v1 51 | kind: ClusterRole 52 | metadata: 53 | labels: 54 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-edit: "true" 55 | name: admission-webhook-kubeflow-poddefaults-edit 56 | # Commenting out rules: [] due to https://github.com/gtsystem/lightkube/issues/32 57 | #rules: [] 58 | --- 59 | apiVersion: rbac.authorization.k8s.io/v1 60 | kind: ClusterRole 61 | metadata: 62 | labels: 63 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-poddefaults-admin: "true" 64 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-poddefaults-edit: "true" 65 | rbac.authorization.kubeflow.org/aggregate-to-kubeflow-view: "true" 66 | name: admission-webhook-kubeflow-poddefaults-view 67 | rules: 68 | - apiGroups: 69 | - kubeflow.org 70 | resources: 71 | - poddefaults 72 | verbs: 73 | - get 74 | - list 75 | - watch 76 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: > 8 | Thanks for taking the time to fill out this bug report! Before submitting your issue, please make 9 | sure you are using the latest version of the charms. If not, please switch to the newest revision prior to 10 | posting your report to make sure it's not already solved. 11 | - type: textarea 12 | id: bug-description 13 | attributes: 14 | label: Bug Description 15 | description: > 16 | If applicable, add screenshots to help explain your problem. If applicable, add screenshots to 17 | help explain the problem you are facing. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: reproduction 22 | attributes: 23 | label: To Reproduce 24 | description: > 25 | Please provide a step-by-step instruction of how to reproduce the behavior. 26 | placeholder: | 27 | 1. `juju deploy ...` 28 | 2. `juju relate ...` 29 | 3. `juju status --relations` 30 | validations: 31 | required: true 32 | - type: textarea 33 | id: environment 34 | attributes: 35 | label: Environment 36 | description: > 37 | We need to know a bit more about the context in which you run the charm. 38 | - Are you running Juju locally, on lxd, in multipass or on some other platform? 39 | - What track and channel you deployed the charm from (ie. `latest/edge` or similar). 40 | - Version of any applicable components, like the juju snap, the model controller, lxd, microk8s, and/or multipass. 41 | validations: 42 | required: true 43 | - type: textarea 44 | id: logs 45 | attributes: 46 | label: Relevant Log Output 47 | description: > 48 | Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 49 | Fetch the logs using `juju debug-log --replay` and `kubectl logs ...`. Additional details available in the juju docs 50 | at https://juju.is/docs/olm/juju-logs 51 | render: shell 52 | validations: 53 | required: true 54 | - type: textarea 55 | id: additional-context 56 | attributes: 57 | label: Additional Context 58 | -------------------------------------------------------------------------------- /.github/workflows/integrate.yaml: -------------------------------------------------------------------------------- 1 | # reusable workflow triggered by other actions 2 | name: CI 3 | 4 | on: 5 | workflow_call: 6 | secrets: 7 | CHARMCRAFT_CREDENTIALS: 8 | required: true 9 | 10 | jobs: 11 | lib-check: 12 | name: Check libraries 13 | uses: canonical/charmed-kubeflow-workflows/.github/workflows/_quality-checks.yaml@main 14 | secrets: inherit 15 | with: 16 | charm-path: "." 17 | 18 | lint: 19 | name: Lint 20 | runs-on: ubuntu-24.04 21 | 22 | steps: 23 | - name: Check out code 24 | uses: actions/checkout@v4 25 | 26 | - name: Install dependencies 27 | run: pipx install tox 28 | - name: Lint code 29 | run: tox -vve lint 30 | 31 | unit: 32 | name: Unit Test 33 | runs-on: ubuntu-24.04 34 | 35 | steps: 36 | - name: Check out code 37 | uses: actions/checkout@v4 38 | 39 | - name: Install dependencies 40 | run: pipx install tox 41 | 42 | - name: Run unit tests 43 | run: tox -e unit 44 | 45 | terraform-checks: 46 | name: Terraform 47 | uses: canonical/charmed-kubeflow-workflows/.github/workflows/terraform-checks.yaml@main 48 | with: 49 | charm-path: . 50 | 51 | integration: 52 | name: Integration Tests 53 | runs-on: ubuntu-24.04 54 | steps: 55 | - name: Check out code 56 | uses: actions/checkout@v4 57 | 58 | - name: Install dependencies 59 | run: pipx install tox 60 | 61 | - name: Setup environment 62 | run: | 63 | sudo apt-get remove -y docker-ce docker-ce-cli containerd.io 64 | sudo rm -rf /run/containerd 65 | sudo snap install concierge --classic 66 | sudo concierge prepare --trace 67 | 68 | - name: Test 69 | run: | 70 | tox -vve integration -- --model testing 71 | 72 | # On failure, capture debugging resources 73 | - name: Get all 74 | run: kubectl get all -A 75 | if: failure() 76 | 77 | - name: Describe deployments 78 | run: kubectl describe deployments -A 79 | if: failure() 80 | 81 | - name: Describe replicasets 82 | run: kubectl describe replicasets -A 83 | if: failure() 84 | 85 | - name: Get juju status 86 | run: juju status 87 | if: failure() 88 | 89 | - name: Get application logs 90 | run: kubectl logs -n testing --tail 1000 -ljuju-app=admission-webhook 91 | if: failure() 92 | 93 | - name: Get application operator logs 94 | run: kubectl logs -n testing --tail 1000 -ljuju-operator=admission-webhook 95 | if: failure() 96 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | [flake8] 5 | max-line-length = 100 6 | 7 | [tox] 8 | skipsdist = True 9 | skip_missing_interpreters = True 10 | envlist = fmt, lint, integration 11 | 12 | [vars] 13 | all_path = {[vars]src_path} {[vars]tst_path} 14 | src_path = {toxinidir}/src/ 15 | tst_path = {toxinidir}/tests/ 16 | 17 | [testenv] 18 | passenv = 19 | PYTHONPATH 20 | CHARM_BUILD_DIR 21 | MODEL_SETTINGS 22 | KUBECONFIG 23 | setenv = 24 | PYTHONPATH = {toxinidir}:{toxinidir}/lib:{[vars]src_path} 25 | PYTHONBREAKPOINT=ipdb.set_trace 26 | PY_COLORS=1 27 | deps = 28 | poetry>=2.1.3 29 | 30 | [testenv:update-requirements] 31 | commands = 32 | # updating all groups' locked dependencies: 33 | poetry lock --regenerate 34 | description = Update requirements 35 | skip_install = true 36 | 37 | [testenv:fmt] 38 | commands = 39 | isort {[vars]all_path} 40 | black {[vars]all_path} 41 | description = Apply coding style standards to code 42 | commands_pre = 43 | poetry install --only fmt 44 | skip_install = true 45 | 46 | [testenv:lint] 47 | commands = 48 | # uncomment the following line if this charm owns a lib 49 | # codespell {[vars]lib_path} 50 | codespell {toxinidir}/. --skip {toxinidir}/./.git --skip {toxinidir}/./.tox \ 51 | --skip {toxinidir}/./build --skip {toxinidir}/./lib --skip {toxinidir}/./venv \ 52 | --skip {toxinidir}/./.mypy_cache \ 53 | --skip {toxinidir}/./icon.svg --skip *.json.tmpl \ 54 | --skip *.lock 55 | # pflake8 wrapper supports config from pyproject.toml 56 | pflake8 {[vars]all_path} 57 | isort --check-only --diff {[vars]all_path} 58 | black --check --diff {[vars]all_path} 59 | description = Check code against coding style standards 60 | commands_pre = 61 | poetry install --only lint 62 | skip_install = true 63 | 64 | [testenv:tflint] 65 | allowlist_externals = 66 | tflint 67 | commands = 68 | tflint --chdir=terraform --recursive 69 | description = Check Terraform code against coding style standards 70 | 71 | [testenv:unit] 72 | commands = 73 | coverage run --source={[vars]src_path} \ 74 | -m pytest --ignore={[vars]tst_path}integration -vv --tb native {posargs} 75 | coverage report 76 | coverage xml 77 | description = Run unit tests 78 | commands_pre = 79 | poetry install --only unit,charm 80 | skip_install = true 81 | 82 | [testenv:integration] 83 | commands = pytest -v --tb native --asyncio-mode=auto {[vars]tst_path}integration --log-cli-level=INFO -s {posargs} 84 | description = Run integration tests 85 | commands_pre = 86 | poetry install --only integration 87 | skip_install = true 88 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | # Testing tools configuration 5 | [tool.coverage.run] 6 | branch = true 7 | 8 | [tool.coverage.report] 9 | show_missing = true 10 | 11 | [tool.pytest.ini_options] 12 | minversion = "6.0" 13 | log_cli_level = "INFO" 14 | 15 | # Formatting tools configuration 16 | [tool.black] 17 | line-length = 99 18 | target-version = ["py38"] 19 | 20 | [tool.isort] 21 | line_length = 99 22 | profile = "black" 23 | 24 | # Linting tools configuration 25 | [tool.flake8] 26 | max-line-length = 99 27 | max-doc-length = 99 28 | max-complexity = 10 29 | exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] 30 | select = ["E", "W", "F", "C", "N", "R", "D", "H"] 31 | # Ignore W503, E501 because using black creates errors with this 32 | # Ignore D107 Missing docstring in __init__ 33 | ignore = ["W503", "E501", "D107"] 34 | # D100, D101, D102, D103: Ignore missing docstrings in tests 35 | per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] 36 | docstring-convention = "google" 37 | # Check for properly formatted copyright header in each file 38 | copyright-check = "True" 39 | copyright-author = "Canonical Ltd." 40 | copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" 41 | 42 | [tool.poetry] 43 | package-mode = false 44 | 45 | [tool.poetry.group.charm] 46 | optional = true 47 | 48 | [tool.poetry.group.charm.dependencies] 49 | charmed-kubeflow-chisme = ">=0.4.14" 50 | cosl = "^0.0.45" 51 | lightkube = "^0.15.6" 52 | ops = "^2.17.1" 53 | pyyaml = "^6.0.2" 54 | 55 | [tool.poetry.group.fmt] 56 | optional = true 57 | 58 | [tool.poetry.group.fmt.dependencies] 59 | black = "^24.8.0" 60 | isort = "^5.13.2" 61 | 62 | [tool.poetry.group.lint] 63 | optional = true 64 | 65 | [tool.poetry.group.lint.dependencies] 66 | black = "^24.8.0" 67 | codespell = "^2.3.0" 68 | flake8 = "^7.0.0" 69 | flake8-builtins = "^2.5.0" 70 | flake8-copyright = "^0.2.4" 71 | isort = "^5.13.2" 72 | pep8-naming = "^0.14.1" 73 | pyproject-flake8 = "^7.0.0" 74 | 75 | [tool.poetry.group.unit] 76 | optional = true 77 | 78 | [tool.poetry.group.unit.dependencies] 79 | coverage = "^7.6.1" 80 | ops = "^2.17.1" 81 | pytest = "^8.3.4" 82 | pytest-mock = "^3.14.0" 83 | 84 | [tool.poetry.group.integration] 85 | optional = true 86 | 87 | [tool.poetry.group.integration.dependencies] 88 | charmed-kubeflow-chisme = ">=0.4.14" 89 | juju = "<4.0" 90 | lightkube = "^0.15.6" 91 | pytest-operator = "^0.38.0" 92 | pyyaml = "^6.0.2" 93 | tenacity = "^9.0.0" 94 | 95 | [project] 96 | name = "admission-webhook-operator" 97 | requires-python = ">=3.12,<4.0" 98 | -------------------------------------------------------------------------------- /terraform/README.md: -------------------------------------------------------------------------------- 1 | # Terraform module for admission-webhook 2 | 3 | This is a Terraform module facilitating the deployment of the admission-webhook charm, using the [Terraform juju provider](https://github.com/juju/terraform-provider-juju/). For more information, refer to the provider [documentation](https://registry.terraform.io/providers/juju/juju/latest/docs). 4 | 5 | ## Requirements 6 | This module requires a `juju` model to be available. Refer to the [usage section](#usage) below for more details. 7 | 8 | ## API 9 | 10 | ### Inputs 11 | The module offers the following configurable inputs: 12 | 13 | | Name | Type | Description | Required | 14 | | - | - | - | - | 15 | | `app_name`| string | Application name | False | 16 | | `base`| string | Charm base | False | 17 | | `channel`| string | Channel that the charm is deployed from | False | 18 | | `config`| map(string) | Map of the charm configuration options | False | 19 | | `model_name`| string | Name of the model that the charm is deployed on | True | 20 | | `resources`| map(string) | Map of the charm resources | False | 21 | | `revision`| number | Revision number of the charm name | False | 22 | 23 | ### Outputs 24 | Upon applied, the module exports the following outputs: 25 | 26 | | Name | Description | 27 | | - | - | 28 | | `app_name`| Application name | 29 | | `provides`| Map of `provides` endpoints | 30 | | `requires`| Map of `reqruires` endpoints | 31 | 32 | ## Usage 33 | 34 | This module is intended to be used as part of a higher-level module. When defining one, users should ensure that Terraform is aware of the `juju_model` dependency of the charm module. There are two options to do so when creating a high-level module: 35 | 36 | ### Define a `juju_model` resource 37 | Define a `juju_model` resource and pass to the `model_name` input a reference to the `juju_model` resource's name. For example: 38 | 39 | ``` 40 | resource "juju_model" "testing" { 41 | name = kubeflow 42 | } 43 | 44 | module "admission-webhook" { 45 | source = "" 46 | model_name = juju_model.testing.name 47 | } 48 | ``` 49 | 50 | ### Define a `data` source 51 | Define a `data` source and pass to the `model_name` input a reference to the `data.juju_model` resource's name. This will enable Terraform to look for a `juju_model` resource with a name attribute equal to the one provided, and apply only if this is present. Otherwise, it will fail before applying anything. 52 | ``` 53 | data "juju_model" "testing" { 54 | name = var.model_name 55 | } 56 | 57 | module "admission-webhook" { 58 | source = "" 59 | model_name = data.juju_model.testing.name 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /src/certs.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | from pathlib import Path 3 | from subprocess import check_call 4 | 5 | SSL_CONFIG_FILE = "src/templates/ssl.conf.j2" 6 | 7 | 8 | def gen_certs(model: str, service_name: str): 9 | """Generate certificates.""" 10 | 11 | ssl_conf = Path(SSL_CONFIG_FILE).read_text() 12 | ssl_conf = ssl_conf.replace("{{ model }}", str(model)) 13 | ssl_conf = ssl_conf.replace("{{ service_name }}", str(service_name)) 14 | 15 | with tempfile.TemporaryDirectory() as tmp_dir: 16 | tmp_path = Path(tmp_dir) 17 | (tmp_path / "ssl.conf").write_text(ssl_conf) 18 | 19 | # execute OpenSSL commands 20 | check_call(["openssl", "genrsa", "-out", tmp_path / "ca.key", "2048"]) 21 | check_call(["openssl", "genrsa", "-out", tmp_path / "server.key", "2048"]) 22 | check_call( 23 | [ 24 | "openssl", 25 | "req", 26 | "-x509", 27 | "-new", 28 | "-sha256", 29 | "-nodes", 30 | "-days", 31 | "3650", 32 | "-key", 33 | tmp_path / "ca.key", 34 | "-subj", 35 | "/CN=127.0.0.1", 36 | "-out", 37 | tmp_path / "ca.crt", 38 | ] 39 | ) 40 | check_call( 41 | [ 42 | "openssl", 43 | "req", 44 | "-new", 45 | "-sha256", 46 | "-key", 47 | tmp_path / "server.key", 48 | "-out", 49 | tmp_path / "server.csr", 50 | "-config", 51 | tmp_path / "ssl.conf", 52 | ] 53 | ) 54 | check_call( 55 | [ 56 | "openssl", 57 | "x509", 58 | "-req", 59 | "-sha256", 60 | "-in", 61 | tmp_path / "server.csr", 62 | "-CA", 63 | tmp_path / "ca.crt", 64 | "-CAkey", 65 | tmp_path / "ca.key", 66 | "-CAcreateserial", 67 | "-out", 68 | tmp_path / "cert.pem", 69 | "-days", 70 | "365", 71 | "-extensions", 72 | "v3_ext", 73 | "-extfile", 74 | tmp_path / "ssl.conf", 75 | ] 76 | ) 77 | 78 | ret_certs = { 79 | "cert": (tmp_path / "cert.pem").read_text(), 80 | "key": (tmp_path / "server.key").read_text(), 81 | "ca": (tmp_path / "ca.crt").read_text(), 82 | } 83 | 84 | # cleanup temporary files 85 | for file in tmp_path.glob("cert-gen-*"): 86 | file.unlink() 87 | 88 | return ret_certs 89 | -------------------------------------------------------------------------------- /icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 51 | 55 | 58 | 63 | 68 | 73 | 78 | 83 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | # reusable workflow for publishing all charms in this repo 2 | name: Publish 3 | 4 | on: 5 | workflow_call: 6 | inputs: 7 | source_branch: 8 | description: Github branch from this repo to publish. If blank, will use the default branch 9 | default: '' 10 | required: false 11 | type: string 12 | secrets: 13 | CHARMCRAFT_CREDENTIALS: 14 | required: true 15 | workflow_dispatch: 16 | inputs: 17 | destination_channel: 18 | description: CharmHub channel to publish to 19 | required: false 20 | default: 'latest/edge' 21 | type: string 22 | source_branch: 23 | description: Github branch from this repo to publish. If blank, will use the default branch 24 | required: false 25 | default: '' 26 | type: string 27 | 28 | jobs: 29 | get-charm-paths: 30 | name: Generate the Charm Matrix 31 | runs-on: ubuntu-24.04 32 | outputs: 33 | charm_paths_list: ${{ steps.get-charm-paths.outputs.CHARM_PATHS_LIST }} 34 | steps: 35 | - uses: actions/checkout@v4 36 | with: 37 | fetch-depth: 0 38 | ref: ${{ inputs.source_branch }} 39 | - name: Get paths for all charms in repo 40 | id: get-charm-paths 41 | run: bash .github/workflows/get-charm-paths.sh 42 | 43 | 44 | publish-charm: 45 | name: Publish Charm 46 | runs-on: ubuntu-24.04 47 | needs: get-charm-paths 48 | strategy: 49 | fail-fast: false 50 | matrix: 51 | charm-path: ${{ fromJson(needs.get-charm-paths.outputs.charm_paths_list) }} 52 | 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v4 56 | with: 57 | fetch-depth: 0 58 | ref: ${{ inputs.source_branch }} 59 | 60 | - name: Select charmhub channel 61 | uses: canonical/charming-actions/channel@2.6.2 62 | id: select-channel 63 | if: ${{ inputs.destination_channel == '' }} 64 | 65 | # Combine inputs from different sources to a single canonical value so later steps don't 66 | # need logic for picking the right one 67 | - name: Parse and combine inputs 68 | id: parse-inputs 69 | run: | 70 | # destination_channel 71 | destination_channel="${{ inputs.destination_channel || steps.select-channel.outputs.name }}" 72 | echo "setting output of destination_channel=$destination_channel" 73 | echo "::set-output name=destination_channel::$destination_channel" 74 | 75 | # tag_prefix 76 | # if charm_path = ./ --> tag_prefix = '' (null) 77 | # if charm_path != ./some-charm (eg: a charm in a ./charms dir) --> tag_prefix = 'some-charm' 78 | if [ ${{ matrix.charm-path }} == './' ]; then 79 | tag_prefix='' 80 | else 81 | tag_prefix=$(basename ${{ matrix.charm-path }} ) 82 | fi 83 | echo "setting output of tag_prefix=$tag_prefix" 84 | echo "::set-output name=tag_prefix::$tag_prefix" 85 | 86 | # Required to charmcraft pack in non-destructive mode 87 | - name: Setup lxd 88 | uses: canonical/setup-lxd@v0.1.2 89 | with: 90 | channel: latest/stable 91 | 92 | - name: Upload charm to charmhub 93 | uses: canonical/charming-actions/upload-charm@2.6.2 94 | with: 95 | credentials: ${{ secrets.CHARMCRAFT_CREDENTIALS }} 96 | github-token: ${{ secrets.GITHUB_TOKEN }} 97 | charm-path: ${{ matrix.charm-path }} 98 | channel: ${{ steps.parse-inputs.outputs.destination_channel }} 99 | tag-prefix: ${{ steps.parse-inputs.outputs.tag_prefix }} 100 | charmcraft-channel: 3.x/stable 101 | destructive-mode: false 102 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Overview 4 | 5 | This document outlines the processes and practices recommended for contributing enhancements to ``admission-webhook``. 6 | 7 | ## Talk to us First 8 | 9 | Before developing enhancements to this charm, you should [open an issue](/../../issues) explaining your use case. If you would like to chat with us about your use-cases or proposed implementation, you can reach us at [MLOps Mattermost public channel](https://chat.charmhub.io/charmhub/channels/mlops-documentation) or on [Discourse](https://discourse.charmhub.io/). 10 | 11 | ## Pull Requests 12 | 13 | Please help us out in ensuring easy to review branches by rebasing your pull request branch onto the `main` branch. This also avoids merge commits and creates a linear Git commit history. 14 | 15 | All pull requests require review before being merged. Code review typically examines: 16 | - code quality 17 | - test coverage 18 | - user experience for Juju administrators of this charm. 19 | 20 | ## Recommended Knowledge 21 | 22 | Familiarising yourself with the [Charmed Operator Framework](https://juju.is/docs/sdk) library will help you a lot when working on new features or bug fixes. 23 | 24 | ## Build Charm 25 | 26 | To build ``admission-webhook`` run: 27 | 28 | ```shell 29 | charmcraft pack 30 | ``` 31 | 32 | ## Developing 33 | 34 | You can use the environments created by `tox` for development. For example, to load the `unit` environment into your shell, run: 35 | 36 | ```shell 37 | tox --notest -e unit 38 | source .tox/unit/bin/activate 39 | ``` 40 | 41 | ### Testing 42 | 43 | Use tox for testing. For example to test the `integration` environment, run: 44 | 45 | ```shell 46 | tox -e integration 47 | ``` 48 | 49 | See `tox.ini` for all available environments. 50 | 51 | ### Deploy 52 | 53 | ```bash 54 | # Create a model 55 | juju add-model dev 56 | # Enable DEBUG logging 57 | juju model-config logging-config="=INFO;unit=DEBUG" 58 | # Deploy the charm 59 | juju deploy ./admission-webhook_ubuntu@24.04-amd64.charm \ 60 | --resource oci-image=$(yq '.resources."oci-image"."upstream-source"' metadata.yaml) 61 | 62 | ## Canonical Contributor Agreement 63 | 64 | Canonical welcomes contributions to this charm. Please check out our [contributor agreement](https://ubuntu.com/legal/contributors) if you're interested in contributing. 65 | 66 | ## How to Manage Python Dependencies and Environments 67 | 68 | 69 | ### Prerequisites 70 | 71 | `tox` is the only tool required locally, as `tox` internally installs and uses `poetry`, be it to manage Python dependencies or to run `tox` environments. To install it: `pipx install tox`. 72 | 73 | Optionally, `poerty` can be additionally installed independently just for the sake of running Python commands locally outside of `tox` during debugging/development. To install it: `pipx install poetry`. 74 | 75 | 76 | ### Updating Dependencies 77 | 78 | To add/update/remove any dependencies and/or to upgrade Python, simply: 79 | 80 | 1. add/update/remove such dependencies to/in/from the desired group(s) below `[tool.poetry.group..dependencies]` in `pyproject.toml`, and/or upgrade Python itself in `requires-python` under `[project]` 81 | 82 | _⚠️ dependencies for the charm itself are also defined as dependencies of a dedicated group called `charm`, specifically below `[tool.poetry.group.charm.dependencies]`, and not as project dependencies below `[project.dependencies]` or `[tool.poetry.dependencies]` ⚠️_ 83 | 84 | 2. run `tox -e update-requirements` to update the lock file 85 | 86 | by this point, `poerty`, through `tox`, will let you know if there are any dependency conflicts to solve. 87 | 88 | 3. optionally, if you also want to update your local environment for running Python commands/scripts yourself and not through tox, see [Running Python Environments](#running-python-environments) below 89 | 90 | 91 | ### Running `tox` Environments 92 | 93 | To run `tox` environments, either locally for development or in CI workflows for testing, ensure to have `tox` installed first and then simply run your `tox` environments natively (e.g.: `tox -e lint`). `tox` will internally first install `poetry` and then rely on it to install and run its environments. 94 | 95 | 96 | ### Running Python Environments 97 | 98 | To run Python commands locally for debugging/development from any environments built from any combinations of dependency groups without relying on `tox`: 99 | 1. ensure you have `poetry` installed 100 | 2. install any required dependency groups: `poetry install --only ,` (or all groups, if you prefer: `poetry install --all-groups`) 101 | 3. run Python commands via poetry: `poetry run python3 ` 102 | -------------------------------------------------------------------------------- /charmcraft.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | type: charm 5 | platforms: 6 | ubuntu@24.04:amd64: 7 | 8 | # Files implicitly created by charmcraft without a part: 9 | # - dispatch (https://github.com/canonical/charmcraft/pull/1898) 10 | # - manifest.yaml 11 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L259) 12 | # Files implicitly copied/"staged" by charmcraft without a part: 13 | # - actions.yaml, config.yaml, metadata.yaml 14 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L290-L293 15 | # https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/services/package.py#L156-L157) 16 | parts: 17 | # "poetry-deps" part name is a magic constant 18 | # https://github.com/canonical/craft-parts/pull/901 19 | poetry-deps: 20 | plugin: nil 21 | build-packages: 22 | - curl 23 | override-build: | 24 | # Use environment variable instead of `--break-system-packages` to avoid failing on older 25 | # versions of pip that do not recognize `--break-system-packages` 26 | # `--user` needed (in addition to `--break-system-packages`) for Ubuntu >=24.04 27 | PIP_BREAK_SYSTEM_PACKAGES=true python3 -m pip install --user --upgrade pip==24.3.1 # renovate: charmcraft-pip-latest 28 | 29 | # Use uv to install poetry so that a newer version of Python can be installed if needed by poetry 30 | curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/uv/releases/download/0.5.15/uv-installer.sh | sh # renovate: charmcraft-uv-latest 31 | # poetry 2.0.0 requires Python >=3.9 32 | if ! "$HOME/.local/bin/uv" python find '>=3.9' 33 | then 34 | # Use first Python version that is >=3.9 and available in an Ubuntu LTS 35 | # (to reduce the number of Python versions we use) 36 | "$HOME/.local/bin/uv" python install 3.10.12 # renovate: charmcraft-python-ubuntu-22.04 37 | fi 38 | "$HOME/.local/bin/uv" tool install --no-python-downloads --python '>=3.9' poetry==2.0.0 --with poetry-plugin-export==1.8.0 # renovate: charmcraft-poetry-latest 39 | 40 | ln -sf "$HOME/.local/bin/poetry" /usr/local/bin/poetry 41 | # "charm-poetry" part name is arbitrary; use for consistency 42 | # Avoid using "charm" part name since that has special meaning to charmcraft 43 | charm-poetry: 44 | # By default, the `poetry` plugin creates/stages these directories: 45 | # - lib, src 46 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L76-L78) 47 | # - venv 48 | # (https://github.com/canonical/charmcraft/blob/9ff19c328e23b50cc06f04e8a5ad4835740badf4/charmcraft/parts/plugins/_poetry.py#L95 49 | # https://github.com/canonical/craft-parts/blob/afb0d652eb330b6aaad4f40fbd6e5357d358de47/craft_parts/plugins/base.py#L270) 50 | plugin: poetry 51 | source: . 52 | after: 53 | - poetry-deps 54 | poetry-export-extra-args: ['--only', 'charm'] 55 | build-packages: 56 | - libffi-dev # Needed to build Python dependencies with Rust from source 57 | - libssl-dev # Needed to build Python dependencies with Rust from source 58 | - pkg-config # Needed to build Python dependencies with Rust from source 59 | override-build: | 60 | # Workaround for https://github.com/canonical/charmcraft/issues/2068 61 | # rustup used to install rustc and cargo, which are needed to build Python dependencies with Rust from source 62 | if [[ "$CRAFT_PLATFORM" == ubuntu@20.04:* || "$CRAFT_PLATFORM" == ubuntu@22.04:* ]] 63 | then 64 | snap install rustup --classic 65 | else 66 | apt-get install rustup -y 67 | fi 68 | 69 | # If Ubuntu version < 24.04, rustup was installed from snap instead of from the Ubuntu 70 | # archive—which means the rustup version could be updated at any time. Print rustup version 71 | # to build log to make changes to the snap's rustup version easier to track 72 | rustup --version 73 | 74 | # rpds-py (Python package) >=0.19.0 requires rustc >=1.76, which is not available in the 75 | # Ubuntu 22.04 archive. Install rustc and cargo using rustup instead of the Ubuntu archive 76 | rustup set profile minimal 77 | rustup default 1.83.0 # renovate: charmcraft-rust-latest 78 | 79 | craftctl default 80 | # Include requirements.txt in *.charm artifact for easier debugging 81 | cp requirements.txt "$CRAFT_PART_INSTALL/requirements.txt" 82 | # "files" part name is arbitrary; use for consistency 83 | files: 84 | plugin: dump 85 | source: . 86 | stage: 87 | - LICENSE 88 | -------------------------------------------------------------------------------- /tests/integration/test_charm.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | import logging 5 | from pathlib import Path 6 | from time import sleep 7 | 8 | import lightkube 9 | import pytest 10 | import yaml 11 | from charmed_kubeflow_chisme.lightkube.batch import apply_many 12 | from charmed_kubeflow_chisme.testing import ( 13 | GRAFANA_AGENT_APP, 14 | assert_logging, 15 | assert_security_context, 16 | deploy_and_assert_grafana_agent, 17 | generate_container_securitycontext_map, 18 | get_pod_names, 19 | ) 20 | from lightkube import ApiError, Client, codecs 21 | from lightkube.generic_resource import create_namespaced_resource 22 | from lightkube.resources.apiextensions_v1 import CustomResourceDefinition 23 | from lightkube.resources.core_v1 import Namespace, Pod, Service 24 | from pytest_operator.plugin import OpsTest 25 | from tenacity import retry, stop_after_delay, wait_exponential 26 | 27 | log = logging.getLogger(__name__) 28 | 29 | METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) 30 | APP_NAME = METADATA["name"] 31 | CONTAINERS_SECURITY_CONTEXT_MAP = generate_container_securitycontext_map(METADATA) 32 | 33 | 34 | @pytest.mark.abort_on_fail 35 | async def test_build_and_deploy(ops_test: OpsTest): 36 | built_charm_path = await ops_test.build_charm(".") 37 | log.info(f"Built charm {built_charm_path}") 38 | 39 | image_path = METADATA["resources"]["oci-image"]["upstream-source"] 40 | resources = {"oci-image": image_path} 41 | 42 | await ops_test.model.deploy( 43 | entity_url=built_charm_path, 44 | resources=resources, 45 | trust=True, 46 | ) 47 | 48 | # Deploying grafana-agent-k8s and add all relations 49 | await deploy_and_assert_grafana_agent( 50 | ops_test.model, 51 | APP_NAME, 52 | metrics=False, 53 | dashboard=False, 54 | logging=True, 55 | ) 56 | 57 | 58 | async def test_logging(ops_test: OpsTest): 59 | """Test logging is defined in relation data bag.""" 60 | app = ops_test.model.applications[GRAFANA_AGENT_APP] 61 | await assert_logging(app) 62 | 63 | 64 | async def test_is_active(ops_test: OpsTest): 65 | await ops_test.model.wait_for_idle( 66 | apps=["admission-webhook"], 67 | status="active", 68 | raise_on_blocked=True, 69 | raise_on_error=True, 70 | timeout=300, 71 | ) 72 | 73 | 74 | def _safe_load_file_to_text(filename: str): 75 | """Returns the contents of filename if it is an existing file, else it returns filename""" 76 | try: 77 | text = Path(filename).read_text() 78 | except FileNotFoundError: 79 | text = filename 80 | return text 81 | 82 | 83 | @pytest.fixture(scope="session") 84 | def lightkube_client() -> lightkube.Client: 85 | """Initiates the lightkube client with PodDefault crd resource""" 86 | client = lightkube.Client() 87 | create_namespaced_resource( 88 | group="kubeflow.org", version="v1alpha1", kind="PodDefault", plural="poddefaults" 89 | ) 90 | return client 91 | 92 | 93 | @pytest.fixture(scope="function", params=["./tests/integration/poddefault_test_workloads.yaml"]) 94 | def kubernetes_workloads(request, lightkube_client: lightkube.Client): 95 | """Deploys and removes the workloads defined in the workloads file""" 96 | sleep(30) # to overcome this bug https://bugs.launchpad.net/juju/+bug/1981833 97 | try: 98 | workloads = codecs.load_all_yaml(_safe_load_file_to_text(request.param)) 99 | except Exception as e: 100 | log.error(f"Unable to load workloads from {request.param}, ended up with {e}") 101 | 102 | apply_many(lightkube_client, workloads, "test") 103 | log.info("Workloads created") 104 | yield 105 | lightkube_client.delete(Namespace, name="test-admission-webhook-user-namespace") 106 | log.info("Workloads deleted") 107 | 108 | 109 | def test_namespace_selector_poddefault_service_account_token_mounted( 110 | lightkube_client, kubernetes_workloads 111 | ): 112 | validate_token_mounted(lightkube_client, "testpod", "test-admission-webhook-user-namespace") 113 | 114 | 115 | @retry( 116 | wait=wait_exponential(multiplier=1, min=1, max=10), 117 | stop=stop_after_delay(30), 118 | reraise=True, 119 | ) 120 | def validate_token_mounted( 121 | client: lightkube.Client, 122 | pods_name: str, 123 | namespace_name: str, 124 | ): 125 | """Checks if the token was mounted successfully by checking the volumes on pod 126 | Args: 127 | client: Lightkube client 128 | pods_name: Name of the pod 129 | namespace_name: Name of the namespace 130 | """ 131 | pod = client.get(Pod, name=pods_name, namespace=namespace_name) 132 | target_vols = [ 133 | volume.name for volume in pod.spec.volumes if volume.name == "volume-kf-pipeline-token" 134 | ] 135 | assert len(target_vols) == 1 136 | 137 | 138 | @pytest.mark.parametrize("container_name", list(CONTAINERS_SECURITY_CONTEXT_MAP.keys())) 139 | async def test_container_security_context( 140 | ops_test: OpsTest, 141 | lightkube_client: lightkube.Client, 142 | container_name: str, 143 | ): 144 | """Test container security context is correctly set. 145 | 146 | Verify that container spec defines the security context with correct 147 | user ID and group ID. 148 | """ 149 | pod_name = get_pod_names(ops_test.model.name, APP_NAME)[0] 150 | assert_security_context( 151 | lightkube_client, 152 | pod_name, 153 | container_name, 154 | CONTAINERS_SECURITY_CONTEXT_MAP, 155 | ops_test.model.name, 156 | ) 157 | 158 | 159 | @pytest.mark.abort_on_fail 160 | async def test_remove_with_resources_present(ops_test: OpsTest): 161 | """Test remove with all resources deployed. 162 | Verify that all deployed resources that need to be removed are removed. 163 | """ 164 | 165 | # remove deployed charm and verify that it is removed 166 | await ops_test.model.remove_application(app_name=APP_NAME, block_until_done=True) 167 | assert APP_NAME not in ops_test.model.applications 168 | 169 | # verify that all resources that were deployed are removed 170 | lightkube_client = Client() 171 | 172 | # verify all CRDs in namespace are removed 173 | crd_list = lightkube_client.list( 174 | CustomResourceDefinition, 175 | labels=[("app.juju.is/created-by", "admission-webhook")], 176 | namespace=ops_test.model.name, 177 | ) 178 | assert not list(crd_list) 179 | 180 | # verify that Service is removed 181 | try: 182 | _ = lightkube_client.get( 183 | Service, 184 | name="admission-webhook", 185 | namespace=ops_test.model.name, 186 | ) 187 | except ApiError as error: 188 | if error.status.code != 404: 189 | # other error than Not Found 190 | assert False 191 | -------------------------------------------------------------------------------- /tests/unit/test_operator.py: -------------------------------------------------------------------------------- 1 | """Unit tests for Admission Webhook Charm.""" 2 | 3 | from unittest.mock import MagicMock, patch 4 | 5 | import pytest 6 | from ops.model import ActiveStatus, MaintenanceStatus, WaitingStatus 7 | from ops.pebble import CheckStatus 8 | from ops.testing import Harness 9 | 10 | from charm import AdmissionWebhookCharm 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def harness() -> Harness: 15 | """Create and return Harness for testing.""" 16 | harness = Harness(AdmissionWebhookCharm) 17 | 18 | # setup container networking simulation 19 | harness.set_can_connect("admission-webhook", True) 20 | 21 | return harness 22 | 23 | 24 | class TestCharm: 25 | """Test class for Admission Webhook.""" 26 | 27 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 28 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 29 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 30 | def test_log_forwarding( 31 | self, 32 | k8s_resource_handler: MagicMock, 33 | crd_resource_handler: MagicMock, 34 | harness: Harness, 35 | ): 36 | """Test LogForwarder initialization.""" 37 | with patch("charm.LogForwarder") as mock_logging: 38 | harness.begin() 39 | mock_logging.assert_called_once_with(charm=harness.charm) 40 | 41 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 42 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 43 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 44 | def test_not_leader( 45 | self, 46 | k8s_resource_handler: MagicMock, 47 | crd_resource_handler: MagicMock, 48 | harness: Harness, 49 | ): 50 | """Test not a leader scenario.""" 51 | harness.begin_with_initial_hooks() 52 | harness.container_pebble_ready("admission-webhook") 53 | assert harness.charm.model.unit.status == WaitingStatus("Waiting for leadership") 54 | 55 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 56 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 57 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 58 | def test_no_relation( 59 | self, 60 | k8s_resource_handler: MagicMock, 61 | crd_resource_handler: MagicMock, 62 | harness: Harness, 63 | ): 64 | """Test no relation scenario.""" 65 | harness.set_leader(True) 66 | harness.add_oci_resource( 67 | "oci-image", 68 | { 69 | "registrypath": "ci-test", 70 | "username": "", 71 | "password": "", 72 | }, 73 | ) 74 | 75 | harness.add_storage("certs") 76 | harness.begin_with_initial_hooks() 77 | harness.container_pebble_ready("admission-webhook") 78 | assert harness.charm.model.unit.status == ActiveStatus("") 79 | 80 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 81 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 82 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 83 | def test_pebble_layer( 84 | self, 85 | k8s_resource_handler: MagicMock, 86 | crd_resource_handler: MagicMock, 87 | harness: Harness, 88 | ): 89 | """Test creation of Pebble layer. Only testing specific items.""" 90 | harness.set_leader(True) 91 | harness.set_model_name("test_kubeflow") 92 | harness.add_storage("certs") 93 | harness.begin_with_initial_hooks() 94 | harness.container_pebble_ready("admission-webhook") 95 | pebble_plan = harness.get_container_pebble_plan("admission-webhook") 96 | assert pebble_plan 97 | assert pebble_plan._services 98 | pebble_plan_info = pebble_plan.to_dict() 99 | assert pebble_plan_info["services"]["admission-webhook"]["command"] == "/webhook" 100 | 101 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 102 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 103 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 104 | def test_apply_k8s_resources_success( 105 | self, 106 | k8s_resource_handler: MagicMock, 107 | crd_resource_handler: MagicMock, 108 | harness: Harness, 109 | ): 110 | """Test if K8S resource handler is executed as expected.""" 111 | harness.begin() 112 | harness.charm._apply_k8s_resources() 113 | crd_resource_handler.apply.assert_called() 114 | k8s_resource_handler.apply.assert_called() 115 | assert isinstance(harness.charm.model.unit.status, MaintenanceStatus) 116 | 117 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 118 | @patch("charm.AdmissionWebhookCharm._get_check_status") 119 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 120 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 121 | @pytest.mark.parametrize( 122 | "health_check_status, charm_status", 123 | [ 124 | (CheckStatus.UP, ActiveStatus("")), 125 | (CheckStatus.DOWN, MaintenanceStatus("Workload failed health check")), 126 | ], 127 | ) 128 | def test_update_status( 129 | self, 130 | crd_resource_handler: MagicMock, 131 | k8s_resource_handler: MagicMock, 132 | _get_check_status: MagicMock, 133 | health_check_status, 134 | charm_status, 135 | harness: Harness, 136 | ): 137 | """ 138 | Test update status handler. 139 | Check on the correct charm status when health check status is UP/DOWN. 140 | """ 141 | harness.set_leader(True) 142 | harness.begin_with_initial_hooks() 143 | harness.container_pebble_ready("admission-webhook") 144 | 145 | _get_check_status.return_value = health_check_status 146 | 147 | # test successful update status 148 | harness.charm.on.update_status.emit() 149 | assert harness.charm.model.unit.status == charm_status 150 | 151 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 152 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 153 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 154 | @patch("charm.update_layer") 155 | def test_container_not_reachable_install( 156 | self, 157 | mocked_update_layer, 158 | crd_resource_handler: MagicMock, 159 | k8s_resource_handler: MagicMock, 160 | harness: Harness, 161 | ): 162 | """ 163 | Checks that when the container is not reachable and install hook fires: 164 | * unit status is set to MaintenanceStatus('Pod startup is not complete'). 165 | * a warning is logged with "Connection cannot be established with container". 166 | * update_layer is not called. 167 | """ 168 | # Arrange 169 | harness.set_leader(True) 170 | harness.set_can_connect("admission-webhook", False) 171 | harness.begin() 172 | 173 | # Mock the logger 174 | harness.charm.logger = MagicMock() 175 | 176 | # Act 177 | harness.charm.on.install.emit() 178 | 179 | # Assert 180 | assert harness.charm.model.unit.status == MaintenanceStatus("Pod startup is not complete") 181 | harness.charm.logger.warning.assert_called_with( 182 | "Connection cannot be established with container" 183 | ) 184 | mocked_update_layer.assert_not_called() 185 | 186 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 187 | @patch("charm.AdmissionWebhookCharm.k8s_resource_handler") 188 | @patch("charm.AdmissionWebhookCharm.crd_resource_handler") 189 | @patch("charm.update_layer") 190 | def test_storage_not_available_install( 191 | self, 192 | mocked_update_layer, 193 | crd_resource_handler: MagicMock, 194 | k8s_resource_handler: MagicMock, 195 | harness: Harness, 196 | ): 197 | """ 198 | Checks that when the container is not reachable and install hook fires: 199 | * unit status is set to MaintenanceStatus('Pod startup is not complete'). 200 | * a warning is logged with "Cannot upload certificates: Failed to connect with container". 201 | * update_layer is not called. 202 | """ 203 | # Arrange 204 | harness.set_leader(True) 205 | harness.set_can_connect("admission-webhook", True) 206 | harness.begin() 207 | 208 | # Mock the logger 209 | harness.charm.logger = MagicMock() 210 | 211 | # Act 212 | harness.charm.on.install.emit() 213 | 214 | # Assert 215 | assert harness.charm.model.unit.status == WaitingStatus("Waiting for storage") 216 | harness.charm.logger.info.assert_called_with("Storage not yet available") 217 | mocked_update_layer.assert_not_called() 218 | 219 | @patch("charm.KubernetesServicePatch", lambda x, y, service_name: None) 220 | @pytest.mark.parametrize( 221 | "cert_data_dict, should_certs_refresh", 222 | [ 223 | # Cases where we should generate a new cert 224 | # No cert data, we should refresh certs 225 | ({}, True), 226 | # We are missing one of the required cert data fields, we should refresh certs 227 | ({"ca": "x", "key": "x"}, True), 228 | ({"cert": "x", "key": "x"}, True), 229 | ({"cert": "x", "ca": "x"}, True), 230 | # Cases where we should not generate a new cert 231 | # Cert data already exists, we should not refresh certs 232 | ( 233 | { 234 | "cert": "x", 235 | "ca": "x", 236 | "key": "x", 237 | }, 238 | False, 239 | ), 240 | ], 241 | ) 242 | def test_gen_certs_if_missing( 243 | self, cert_data_dict, should_certs_refresh, harness: Harness, mocker 244 | ): 245 | """Test _gen_certs_if_missing. 246 | This tests whether _gen_certs_if_missing: 247 | * generates a new cert if there is no existing one 248 | * does not generate a new cert if there is an existing one 249 | """ 250 | # Arrange 251 | # Mock away gen_certs so the class does not generate any certs unless we want it to 252 | mocked_gen_certs = mocker.patch("charm.AdmissionWebhookCharm._gen_certs", autospec=True) 253 | harness.begin() 254 | mocked_gen_certs.reset_mock() 255 | 256 | # Set any provided cert data to _stored 257 | for k, v in cert_data_dict.items(): 258 | setattr(harness.charm._stored, k, v) 259 | 260 | # Act 261 | harness.charm._gen_certs_if_missing() 262 | 263 | # Assert that we have/have not called refresh_certs, as expected 264 | assert mocked_gen_certs.called == should_certs_refresh 265 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/charms/observability_libs/v1/kubernetes_service_patch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Canonical Ltd. 2 | # See LICENSE file for licensing details. 3 | 4 | """# [DEPRECATED!] KubernetesServicePatch Library. 5 | 6 | The `kubernetes_service_patch` library is DEPRECATED and will be removed in October 2025. 7 | 8 | For patching the Kubernetes service created by Juju during the deployment of a charm, 9 | `ops.Unit.set_ports` functionality should be used instead. 10 | 11 | """ 12 | 13 | import logging 14 | from types import MethodType 15 | from typing import Any, List, Literal, Optional, Union 16 | 17 | from lightkube import ApiError, Client # pyright: ignore 18 | from lightkube.core import exceptions 19 | from lightkube.models.core_v1 import ServicePort, ServiceSpec 20 | from lightkube.models.meta_v1 import ObjectMeta 21 | from lightkube.resources.core_v1 import Service 22 | from lightkube.types import PatchType 23 | from ops import UpgradeCharmEvent 24 | from ops.charm import CharmBase 25 | from ops.framework import BoundEvent, Object 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | # The unique Charmhub library identifier, never change it 30 | LIBID = "0042f86d0a874435adef581806cddbbb" 31 | 32 | # Increment this major API version when introducing breaking changes 33 | LIBAPI = 1 34 | 35 | # Increment this PATCH version before using `charmcraft publish-lib` or reset 36 | # to 0 if you are raising the major API version 37 | LIBPATCH = 13 38 | 39 | ServiceType = Literal["ClusterIP", "LoadBalancer"] 40 | 41 | 42 | class KubernetesServicePatch(Object): 43 | """A utility for patching the Kubernetes service set up by Juju.""" 44 | 45 | def __init__( 46 | self, 47 | charm: CharmBase, 48 | ports: List[ServicePort], 49 | service_name: Optional[str] = None, 50 | service_type: ServiceType = "ClusterIP", 51 | additional_labels: Optional[dict] = None, 52 | additional_selectors: Optional[dict] = None, 53 | additional_annotations: Optional[dict] = None, 54 | *, 55 | refresh_event: Optional[Union[BoundEvent, List[BoundEvent]]] = None, 56 | ): 57 | """Constructor for KubernetesServicePatch. 58 | 59 | Args: 60 | charm: the charm that is instantiating the library. 61 | ports: a list of ServicePorts 62 | service_name: allows setting custom name to the patched service. If none given, 63 | application name will be used. 64 | service_type: desired type of K8s service. Default value is in line with ServiceSpec's 65 | default value. 66 | additional_labels: Labels to be added to the kubernetes service (by default only 67 | "app.kubernetes.io/name" is set to the service name) 68 | additional_selectors: Selectors to be added to the kubernetes service (by default only 69 | "app.kubernetes.io/name" is set to the service name) 70 | additional_annotations: Annotations to be added to the kubernetes service. 71 | refresh_event: an optional bound event or list of bound events which 72 | will be observed to re-apply the patch (e.g. on port change). 73 | The `install` and `upgrade-charm` events would be observed regardless. 74 | """ 75 | logger.warning( 76 | "The ``kubernetes_service_patch v1`` library is DEPRECATED and will be removed " 77 | "in October 2025. For patching the Kubernetes service created by Juju during " 78 | "the deployment of a charm, ``ops.Unit.set_ports`` functionality should be used instead." 79 | ) 80 | super().__init__(charm, "kubernetes-service-patch") 81 | self.charm = charm 82 | self.service_name = service_name or self._app 83 | # To avoid conflicts with the default Juju service, append "-lb" to the service name. 84 | # The Juju application name is retained for the default service created by Juju. 85 | if self.service_name == self._app and service_type == "LoadBalancer": 86 | self.service_name = f"{self._app}-lb" 87 | self.service_type = service_type 88 | self.service = self._service_object( 89 | ports, 90 | self.service_name, 91 | service_type, 92 | additional_labels, 93 | additional_selectors, 94 | additional_annotations, 95 | ) 96 | 97 | # Make mypy type checking happy that self._patch is a method 98 | assert isinstance(self._patch, MethodType) 99 | # Ensure this patch is applied during the 'install' and 'upgrade-charm' events 100 | self.framework.observe(charm.on.install, self._patch) 101 | self.framework.observe(charm.on.upgrade_charm, self._on_upgrade_charm) 102 | self.framework.observe(charm.on.update_status, self._patch) 103 | # Sometimes Juju doesn't clean-up a manually created LB service, 104 | # so we clean it up ourselves just in case. 105 | self.framework.observe(charm.on.remove, self._remove_service) 106 | 107 | # apply user defined events 108 | if refresh_event: 109 | if not isinstance(refresh_event, list): 110 | refresh_event = [refresh_event] 111 | 112 | for evt in refresh_event: 113 | self.framework.observe(evt, self._patch) 114 | 115 | def _service_object( 116 | self, 117 | ports: List[ServicePort], 118 | service_name: Optional[str] = None, 119 | service_type: ServiceType = "ClusterIP", 120 | additional_labels: Optional[dict] = None, 121 | additional_selectors: Optional[dict] = None, 122 | additional_annotations: Optional[dict] = None, 123 | ) -> Service: 124 | """Creates a valid Service representation. 125 | 126 | Args: 127 | ports: a list of ServicePorts 128 | service_name: allows setting custom name to the patched service. If none given, 129 | application name will be used. 130 | service_type: desired type of K8s service. Default value is in line with ServiceSpec's 131 | default value. 132 | additional_labels: Labels to be added to the kubernetes service (by default only 133 | "app.kubernetes.io/name" is set to the service name) 134 | additional_selectors: Selectors to be added to the kubernetes service (by default only 135 | "app.kubernetes.io/name" is set to the service name) 136 | additional_annotations: Annotations to be added to the kubernetes service. 137 | 138 | Returns: 139 | Service: A valid representation of a Kubernetes Service with the correct ports. 140 | """ 141 | if not service_name: 142 | service_name = self._app 143 | labels = {"app.kubernetes.io/name": self._app} 144 | if additional_labels: 145 | labels.update(additional_labels) 146 | selector = {"app.kubernetes.io/name": self._app} 147 | if additional_selectors: 148 | selector.update(additional_selectors) 149 | return Service( 150 | apiVersion="v1", 151 | kind="Service", 152 | metadata=ObjectMeta( 153 | namespace=self._namespace, 154 | name=service_name, 155 | labels=labels, 156 | annotations=additional_annotations, # type: ignore[arg-type] 157 | ), 158 | spec=ServiceSpec( 159 | selector=selector, 160 | ports=ports, 161 | type=service_type, 162 | ), 163 | ) 164 | 165 | def _patch(self, _) -> None: 166 | """Patch the Kubernetes service created by Juju to map the correct port. 167 | 168 | Raises: 169 | PatchFailed: if patching fails due to lack of permissions, or otherwise. 170 | """ 171 | try: 172 | client = Client() # pyright: ignore 173 | except exceptions.ConfigError as e: 174 | logger.warning("Error creating k8s client: %s", e) 175 | return 176 | 177 | try: 178 | if self._is_patched(client): 179 | return 180 | if self.service_name != self._app: 181 | if not self.service_type == "LoadBalancer": 182 | self._delete_and_create_service(client) 183 | else: 184 | self._create_lb_service(client) 185 | client.patch(Service, self.service_name, self.service, patch_type=PatchType.MERGE) 186 | except ApiError as e: 187 | if e.status.code == 403: 188 | logger.error("Kubernetes service patch failed: `juju trust` this application.") 189 | else: 190 | logger.error("Kubernetes service patch failed: %s", str(e)) 191 | else: 192 | logger.info("Kubernetes service '%s' patched successfully", self._app) 193 | 194 | def _delete_and_create_service(self, client: Client): 195 | service = client.get(Service, self._app, namespace=self._namespace) 196 | service.metadata.name = self.service_name # type: ignore[attr-defined] 197 | service.metadata.resourceVersion = service.metadata.uid = None # type: ignore[attr-defined] # noqa: E501 198 | client.delete(Service, self._app, namespace=self._namespace) 199 | client.create(service) 200 | 201 | def _create_lb_service(self, client: Client): 202 | try: 203 | client.get(Service, self.service_name, namespace=self._namespace) 204 | except ApiError: 205 | client.create(self.service) 206 | 207 | def is_patched(self) -> bool: 208 | """Reports if the service patch has been applied. 209 | 210 | Returns: 211 | bool: A boolean indicating if the service patch has been applied. 212 | """ 213 | client = Client() # pyright: ignore 214 | return self._is_patched(client) 215 | 216 | def _is_patched(self, client: Client) -> bool: 217 | # Get the relevant service from the cluster 218 | try: 219 | service = client.get(Service, name=self.service_name, namespace=self._namespace) 220 | except ApiError as e: 221 | if e.status.code == 404 and self.service_name != self._app: 222 | return False 223 | logger.error("Kubernetes service get failed: %s", str(e)) 224 | raise 225 | 226 | # Construct a list of expected ports, should the patch be applied 227 | expected_ports = [(p.port, p.targetPort) for p in self.service.spec.ports] # type: ignore[attr-defined] 228 | # Construct a list in the same manner, using the fetched service 229 | fetched_ports = [ 230 | (p.port, p.targetPort) for p in service.spec.ports # type: ignore[attr-defined] 231 | ] # noqa: E501 232 | return expected_ports == fetched_ports 233 | 234 | def _on_upgrade_charm(self, event: UpgradeCharmEvent): 235 | """Handle the upgrade charm event.""" 236 | # If a charm author changed the service type from LB to ClusterIP across an upgrade, we need to delete the previous LB. 237 | if self.service_type == "ClusterIP": 238 | 239 | client = Client() # pyright: ignore 240 | 241 | # Define a label selector to find services related to the app 242 | selector: dict[str, Any] = {"app.kubernetes.io/name": self._app} 243 | 244 | # Check if any service of type LoadBalancer exists 245 | services = client.list(Service, namespace=self._namespace, labels=selector) 246 | for service in services: 247 | if ( 248 | not service.metadata 249 | or not service.metadata.name 250 | or not service.spec 251 | or not service.spec.type 252 | ): 253 | logger.warning( 254 | "Service patch: skipping resource with incomplete metadata: %s.", service 255 | ) 256 | continue 257 | if service.spec.type == "LoadBalancer": 258 | client.delete(Service, service.metadata.name, namespace=self._namespace) 259 | logger.info(f"LoadBalancer service {service.metadata.name} deleted.") 260 | 261 | # Continue the upgrade flow normally 262 | self._patch(event) 263 | 264 | def _remove_service(self, _): 265 | """Remove a Kubernetes service associated with this charm. 266 | 267 | Specifically designed to delete the load balancer service created by the charm, since Juju only deletes the 268 | default ClusterIP service and not custom services. 269 | 270 | Returns: 271 | None 272 | 273 | Raises: 274 | ApiError: for deletion errors, excluding when the service is not found (404 Not Found). 275 | """ 276 | client = Client() # pyright: ignore 277 | 278 | try: 279 | client.delete(Service, self.service_name, namespace=self._namespace) 280 | logger.info("The patched k8s service '%s' was deleted.", self.service_name) 281 | except ApiError as e: 282 | if e.status.code == 404: 283 | # Service not found, so no action needed 284 | return 285 | # Re-raise for other statuses 286 | raise 287 | 288 | @property 289 | def _app(self) -> str: 290 | """Name of the current Juju application. 291 | 292 | Returns: 293 | str: A string containing the name of the current Juju application. 294 | """ 295 | return self.charm.app.name 296 | 297 | @property 298 | def _namespace(self) -> str: 299 | """The Kubernetes namespace we're running in. 300 | 301 | Returns: 302 | str: A string containing the name of the current Kubernetes namespace. 303 | """ 304 | with open("/var/run/secrets/kubernetes.io/serviceaccount/namespace", "r") as f: 305 | return f.read().strip() 306 | -------------------------------------------------------------------------------- /src/charm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright 2023 Canonical Ltd. 3 | # See LICENSE file for licensing details. 4 | """A Juju Charm for Admission Webhook Operator.""" 5 | import logging 6 | from base64 import b64encode 7 | from pathlib import Path 8 | 9 | from charmed_kubeflow_chisme.exceptions import ErrorWithStatus, GenericCharmRuntimeError 10 | from charmed_kubeflow_chisme.kubernetes import KubernetesResourceHandler 11 | from charmed_kubeflow_chisme.lightkube.batch import delete_many 12 | from charmed_kubeflow_chisme.pebble import update_layer 13 | from charms.loki_k8s.v1.loki_push_api import LogForwarder 14 | from charms.observability_libs.v1.kubernetes_service_patch import KubernetesServicePatch 15 | from lightkube import ApiError 16 | from lightkube.generic_resource import load_in_cluster_generic_resources 17 | from lightkube.models.core_v1 import ServicePort 18 | from ops.charm import CharmBase 19 | from ops.framework import StoredState 20 | from ops.main import main 21 | from ops.model import ActiveStatus, Container, MaintenanceStatus, ModelError, WaitingStatus 22 | from ops.pebble import CheckStatus, Layer, PathError 23 | 24 | from certs import gen_certs 25 | 26 | K8S_RESOURCE_FILES = [ 27 | "src/templates/webhook_configuration.yaml.j2", 28 | "src/templates/auth_manifests.yaml.j2", 29 | ] 30 | 31 | CRD_RESOURCE_FILES = [ 32 | "src/templates/crds.yaml.j2", 33 | ] 34 | 35 | 36 | class AdmissionWebhookCharm(CharmBase): 37 | """A Juju Charm for Admission Webhook Operator.""" 38 | 39 | _stored = StoredState() 40 | 41 | def __init__(self, framework): 42 | """Initialize charm and setup the container.""" 43 | super().__init__(framework) 44 | 45 | # retrieve configuration and base settings 46 | self.logger = logging.getLogger(__name__) 47 | self._container_name = "admission-webhook" 48 | self._certs_storage_name = "certs" 49 | self._container_meta = self.meta.containers[self._container_name] 50 | self._container = self.unit.get_container(self._container_name) 51 | self._port = self.model.config["port"] 52 | self._lightkube_field_manager = "lightkube" 53 | self._exec_command = "/webhook" 54 | self._namespace = self.model.name 55 | self._name = self.model.app.name 56 | self._service_name = self._name 57 | self._k8s_resource_handler = None 58 | self._crd_resource_handler = None 59 | 60 | # setup events to be handled by main event handler 61 | for event in [ 62 | self.on.install, 63 | self.on.config_changed, 64 | self.on.admission_webhook_pebble_ready, 65 | ]: 66 | self.framework.observe(event, self._on_event) 67 | # setup events to be handled by specific event handlers 68 | self.framework.observe(self.on.upgrade_charm, self._on_upgrade) 69 | self.framework.observe(self.on.remove, self._on_remove) 70 | self.framework.observe(self.on.update_status, self._on_update_status) 71 | 72 | # generate certs 73 | self._gen_certs_if_missing() 74 | 75 | port = ServicePort(int(self._port), name=f"{self.app.name}") 76 | self.service_patcher = KubernetesServicePatch( 77 | self, 78 | [port], 79 | service_name=f"{self.model.app.name}", 80 | ) 81 | self._logging = LogForwarder(charm=self) 82 | 83 | @property 84 | def _context(self): 85 | return { 86 | "app_name": self._name, 87 | "namespace": self._namespace, 88 | "port": self._port, 89 | "ca_bundle": b64encode(self._cert_ca.encode("ascii")).decode("utf-8"), 90 | "service_name": self._service_name, 91 | } 92 | 93 | @property 94 | def container(self): 95 | """Return container.""" 96 | return self._container 97 | 98 | @property 99 | def k8s_resource_handler(self): 100 | """Update K8S with K8S resources.""" 101 | if not self._k8s_resource_handler: 102 | self._k8s_resource_handler = KubernetesResourceHandler( 103 | field_manager=self._lightkube_field_manager, 104 | template_files=K8S_RESOURCE_FILES, 105 | context=self._context, 106 | logger=self.logger, 107 | ) 108 | load_in_cluster_generic_resources(self._k8s_resource_handler.lightkube_client) 109 | return self._k8s_resource_handler 110 | 111 | @k8s_resource_handler.setter 112 | def k8s_resource_handler(self, handler: KubernetesResourceHandler): 113 | self._k8s_resource_handler = handler 114 | 115 | @property 116 | def crd_resource_handler(self): 117 | """Update K8S with CRD resources.""" 118 | if not self._crd_resource_handler: 119 | self._crd_resource_handler = KubernetesResourceHandler( 120 | field_manager=self._lightkube_field_manager, 121 | template_files=CRD_RESOURCE_FILES, 122 | context=self._context, 123 | logger=self.logger, 124 | ) 125 | load_in_cluster_generic_resources(self._crd_resource_handler.lightkube_client) 126 | return self._crd_resource_handler 127 | 128 | @property 129 | def _admission_webhook_layer(self) -> Layer: 130 | """Create and return Pebble framework layer.""" 131 | layer_config = { 132 | "summary": "admission-webhook layer", 133 | "description": "Pebble config layer for admission-webhook-operator", 134 | "services": { 135 | self._container_name: { 136 | "override": "replace", 137 | "summary": "Pebble service for admission-webhook-operator", 138 | "startup": "enabled", 139 | "command": self._exec_command, 140 | "on-check-failure": {"admission-webhook-up": "restart"}, 141 | }, 142 | }, 143 | "checks": { 144 | "admission-webhook-up": { 145 | "override": "replace", 146 | "period": "30s", 147 | "timeout": "20s", 148 | "threshold": 4, 149 | "tcp": {"port": self._port}, 150 | } 151 | }, 152 | } 153 | return Layer(layer_config) 154 | 155 | @property 156 | def _cert(self): 157 | return self._stored.cert 158 | 159 | @property 160 | def _cert_key(self): 161 | return self._stored.key 162 | 163 | @property 164 | def _cert_ca(self): 165 | return self._stored.ca 166 | 167 | def _check_leader(self): 168 | """Check if this unit is a leader.""" 169 | if not self.unit.is_leader(): 170 | self.logger.info("Not a leader, skipping setup") 171 | raise ErrorWithStatus("Waiting for leadership", WaitingStatus) 172 | 173 | def _check_storage(self): 174 | """Check if storage is available.""" 175 | certs_storage_path = Path(self._container_meta.mounts[self._certs_storage_name].location) 176 | if not self.container.exists(certs_storage_path): 177 | self.logger.info("Storage not yet available") 178 | raise ErrorWithStatus("Waiting for storage", WaitingStatus) 179 | 180 | def _check_and_report_k8s_conflict(self, error): 181 | """Return True if error status code is 409 (conflict), False otherwise.""" 182 | if error.status.code == 409: 183 | self.logger.warning(f"Encountered a conflict: {error}") 184 | return True 185 | return False 186 | 187 | def _apply_k8s_resources(self, force_conflicts: bool = False) -> None: 188 | """Apply K8S resources. 189 | 190 | Args: 191 | force_conflicts (bool): *(optional)* Will "force" apply requests causing conflicting 192 | fields to change ownership to the field manager used in this 193 | charm. 194 | NOTE: This will only be used if initial regular apply() fails. 195 | """ 196 | self.unit.status = MaintenanceStatus("Creating K8S resources") 197 | try: 198 | self.k8s_resource_handler.apply() 199 | except ApiError as error: 200 | if self._check_and_report_k8s_conflict(error) and force_conflicts: 201 | # conflict detected when applying K8S resources 202 | # re-apply K8S resources with forced conflict resolution 203 | self.unit.status = MaintenanceStatus("Force applying K8S resources") 204 | self.logger.warning("Apply K8S resources with forced changes against conflicts") 205 | self.k8s_resource_handler.apply(force=force_conflicts) 206 | else: 207 | raise GenericCharmRuntimeError("K8S resources creation failed") from error 208 | try: 209 | self.crd_resource_handler.apply() 210 | except ApiError as error: 211 | if self._check_and_report_k8s_conflict(error) and force_conflicts: 212 | # conflict detected when applying CRD resources 213 | # re-apply CRD resources with forced conflict resolution 214 | self.unit.status = MaintenanceStatus("Force applying CRD resources") 215 | self.logger.warning("Apply CRD resources with forced changes against conflicts") 216 | self.crd_resource_handler.apply(force=force_conflicts) 217 | else: 218 | raise GenericCharmRuntimeError("CRD resources creation failed") from error 219 | self.model.unit.status = MaintenanceStatus("K8S resources created") 220 | 221 | def _gen_certs_if_missing(self): 222 | """Generate certificates if they don't already exist in _stored.""" 223 | self.logger.info("Generating certificates if missing.") 224 | cert_attributes = ["cert", "ca", "key"] 225 | # Generate new certs if any cert attribute is missing 226 | for cert_attribute in cert_attributes: 227 | try: 228 | getattr(self._stored, cert_attribute) 229 | except AttributeError: 230 | self._gen_certs() 231 | break 232 | self.logger.info("Certificates already exist.") 233 | 234 | def _gen_certs(self): 235 | """Refresh the certificates, overwriting them if they already existed.""" 236 | self.logger.info("Generating certificates..") 237 | certs = gen_certs(model=self._namespace, service_name=self._service_name) 238 | for k, v in certs.items(): 239 | setattr(self._stored, k, v) 240 | 241 | def _upload_certs_to_container(self, event): 242 | """Upload generated certs to container.""" 243 | certs_storage_path = Path(self._container_meta.mounts[self._certs_storage_name].location) 244 | if not self._certificate_files_exist(): 245 | try: 246 | self.container.push(certs_storage_path / "key.pem", self._cert_key, make_dirs=True) 247 | self.container.push(certs_storage_path / "cert.pem", self._cert, make_dirs=True) 248 | except PathError as e: 249 | raise GenericCharmRuntimeError("Failed to push certs to container") from e 250 | 251 | def _check_container_connection(self, container: Container) -> None: 252 | """Check if connection can be made with container. 253 | Args: 254 | container: the named container in a unit to check. 255 | Raises: 256 | ErrorWithStatus if the connection cannot be made. 257 | """ 258 | if not container.can_connect(): 259 | self.logger.warning("Connection cannot be established with container") 260 | raise ErrorWithStatus("Pod startup is not complete", MaintenanceStatus) 261 | 262 | def _get_check_status(self): 263 | return self.container.get_check("admission-webhook-up").status 264 | 265 | def _refresh_status(self): 266 | """Check leader, refresh status of workload, and set status accordingly.""" 267 | self._check_leader() 268 | try: 269 | check = self._get_check_status() 270 | except ModelError as error: 271 | raise GenericCharmRuntimeError( 272 | "Failed to run health check on workload container" 273 | ) from error 274 | if check != CheckStatus.UP: 275 | self.logger.error( 276 | f"Container {self._container_name} failed health check. It will be restarted." 277 | ) 278 | raise ErrorWithStatus("Workload failed health check", MaintenanceStatus) 279 | else: 280 | self.model.unit.status = ActiveStatus() 281 | 282 | def _on_remove(self, _): 283 | """Remove all resources.""" 284 | delete_error = None 285 | self.unit.status = MaintenanceStatus("Removing K8S resources") 286 | k8s_resources_manifests = self.k8s_resource_handler.render_manifests() 287 | crd_resources_manifests = self.crd_resource_handler.render_manifests() 288 | try: 289 | delete_many(self.k8s_resource_handler.lightkube_client, k8s_resources_manifests) 290 | except ApiError as error: 291 | # do not log/report when resources were not found 292 | if error.status.code != 404: 293 | self.logger.error(f"Failed to delete K8S resources, with error: {error}") 294 | delete_error = error 295 | try: 296 | delete_many(self.crd_resource_handler.lightkube_client, crd_resources_manifests) 297 | except ApiError as error: 298 | # do not log/report when resources were not found 299 | if error.status.code != 404: 300 | self.logger.error(f"Failed to delete CRD resources, with error: {error}") 301 | delete_error = error 302 | 303 | if delete_error is not None: 304 | raise delete_error 305 | 306 | self.unit.status = MaintenanceStatus("K8S resources removed") 307 | 308 | def _on_upgrade(self, _): 309 | """Perform upgrade steps.""" 310 | # force conflict resolution in K8S resources update 311 | self._on_event(_, force_conflicts=True) 312 | 313 | def _on_update_status(self, event): 314 | """Update status actions.""" 315 | try: 316 | self._refresh_status() 317 | except ErrorWithStatus as err: 318 | self.model.unit.status = err.status 319 | 320 | def _certificate_files_exist(self) -> bool: 321 | """Check that the certificate and key files can be pulled from the container.""" 322 | certs_storage_path = Path(self._container_meta.mounts[self._certs_storage_name].location) 323 | try: 324 | self.container.pull(certs_storage_path / "key.pem") 325 | self.container.pull(certs_storage_path / "cert.pem") 326 | return True 327 | except PathError: 328 | return False 329 | 330 | def _on_event(self, event, force_conflicts: bool = False) -> None: 331 | """Perform all required actions for the Charm. 332 | 333 | Args: 334 | force_conflicts (bool): Should only be used when need to resolved conflicts on K8S 335 | resources. 336 | """ 337 | try: 338 | self._check_leader() 339 | self._check_container_connection(self.container) 340 | self._check_storage() 341 | self._apply_k8s_resources(force_conflicts=force_conflicts) 342 | self._upload_certs_to_container(event) 343 | update_layer( 344 | self._container_name, 345 | self._container, 346 | self._admission_webhook_layer, 347 | self.logger, 348 | ) 349 | except ErrorWithStatus as err: 350 | self.model.unit.status = err.status 351 | self.logger.error(f"Failed to handle {event} with error: {err}") 352 | return 353 | 354 | self.model.unit.status = ActiveStatus() 355 | 356 | 357 | if __name__ == "__main__": 358 | main(AdmissionWebhookCharm) 359 | -------------------------------------------------------------------------------- /src/templates/crds.yaml.j2: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | annotations: 5 | controller-gen.kubebuilder.io/version: v0.8.0 6 | creationTimestamp: null 7 | labels: 8 | app: poddefaults 9 | app.kubernetes.io/component: poddefaults 10 | app.kubernetes.io/name: poddefaults 11 | kustomize.component: poddefaults 12 | name: poddefaults.kubeflow.org 13 | spec: 14 | group: kubeflow.org 15 | names: 16 | kind: PodDefault 17 | listKind: PodDefaultList 18 | plural: poddefaults 19 | singular: poddefault 20 | scope: Namespaced 21 | versions: 22 | - name: v1alpha1 23 | schema: 24 | openAPIV3Schema: 25 | properties: 26 | apiVersion: 27 | type: string 28 | kind: 29 | type: string 30 | metadata: 31 | type: object 32 | spec: 33 | properties: 34 | annotations: 35 | additionalProperties: 36 | type: string 37 | type: object 38 | args: 39 | items: 40 | type: string 41 | type: array 42 | automountServiceAccountToken: 43 | type: boolean 44 | command: 45 | items: 46 | type: string 47 | type: array 48 | desc: 49 | type: string 50 | env: 51 | items: 52 | properties: 53 | name: 54 | type: string 55 | value: 56 | type: string 57 | valueFrom: 58 | properties: 59 | configMapKeyRef: 60 | properties: 61 | key: 62 | type: string 63 | name: 64 | type: string 65 | optional: 66 | type: boolean 67 | required: 68 | - key 69 | type: object 70 | fieldRef: 71 | properties: 72 | apiVersion: 73 | type: string 74 | fieldPath: 75 | type: string 76 | required: 77 | - fieldPath 78 | type: object 79 | resourceFieldRef: 80 | properties: 81 | containerName: 82 | type: string 83 | divisor: 84 | anyOf: 85 | - type: integer 86 | - type: string 87 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 88 | x-kubernetes-int-or-string: true 89 | resource: 90 | type: string 91 | required: 92 | - resource 93 | type: object 94 | secretKeyRef: 95 | properties: 96 | key: 97 | type: string 98 | name: 99 | type: string 100 | optional: 101 | type: boolean 102 | required: 103 | - key 104 | type: object 105 | type: object 106 | required: 107 | - name 108 | type: object 109 | type: array 110 | envFrom: 111 | items: 112 | properties: 113 | configMapRef: 114 | properties: 115 | name: 116 | type: string 117 | optional: 118 | type: boolean 119 | type: object 120 | prefix: 121 | type: string 122 | secretRef: 123 | properties: 124 | name: 125 | type: string 126 | optional: 127 | type: boolean 128 | type: object 129 | type: object 130 | type: array 131 | imagePullSecrets: 132 | items: 133 | properties: 134 | name: 135 | type: string 136 | type: object 137 | type: array 138 | initContainers: 139 | items: 140 | properties: 141 | args: 142 | items: 143 | type: string 144 | type: array 145 | command: 146 | items: 147 | type: string 148 | type: array 149 | env: 150 | items: 151 | properties: 152 | name: 153 | type: string 154 | value: 155 | type: string 156 | valueFrom: 157 | properties: 158 | configMapKeyRef: 159 | properties: 160 | key: 161 | type: string 162 | name: 163 | type: string 164 | optional: 165 | type: boolean 166 | required: 167 | - key 168 | type: object 169 | fieldRef: 170 | properties: 171 | apiVersion: 172 | type: string 173 | fieldPath: 174 | type: string 175 | required: 176 | - fieldPath 177 | type: object 178 | resourceFieldRef: 179 | properties: 180 | containerName: 181 | type: string 182 | divisor: 183 | anyOf: 184 | - type: integer 185 | - type: string 186 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 187 | x-kubernetes-int-or-string: true 188 | resource: 189 | type: string 190 | required: 191 | - resource 192 | type: object 193 | secretKeyRef: 194 | properties: 195 | key: 196 | type: string 197 | name: 198 | type: string 199 | optional: 200 | type: boolean 201 | required: 202 | - key 203 | type: object 204 | type: object 205 | required: 206 | - name 207 | type: object 208 | type: array 209 | envFrom: 210 | items: 211 | properties: 212 | configMapRef: 213 | properties: 214 | name: 215 | type: string 216 | optional: 217 | type: boolean 218 | type: object 219 | prefix: 220 | type: string 221 | secretRef: 222 | properties: 223 | name: 224 | type: string 225 | optional: 226 | type: boolean 227 | type: object 228 | type: object 229 | type: array 230 | image: 231 | type: string 232 | imagePullPolicy: 233 | type: string 234 | lifecycle: 235 | properties: 236 | postStart: 237 | properties: 238 | exec: 239 | properties: 240 | command: 241 | items: 242 | type: string 243 | type: array 244 | type: object 245 | httpGet: 246 | properties: 247 | host: 248 | type: string 249 | httpHeaders: 250 | items: 251 | properties: 252 | name: 253 | type: string 254 | value: 255 | type: string 256 | required: 257 | - name 258 | - value 259 | type: object 260 | type: array 261 | path: 262 | type: string 263 | port: 264 | anyOf: 265 | - type: integer 266 | - type: string 267 | x-kubernetes-int-or-string: true 268 | scheme: 269 | type: string 270 | required: 271 | - port 272 | type: object 273 | tcpSocket: 274 | properties: 275 | host: 276 | type: string 277 | port: 278 | anyOf: 279 | - type: integer 280 | - type: string 281 | x-kubernetes-int-or-string: true 282 | required: 283 | - port 284 | type: object 285 | type: object 286 | preStop: 287 | properties: 288 | exec: 289 | properties: 290 | command: 291 | items: 292 | type: string 293 | type: array 294 | type: object 295 | httpGet: 296 | properties: 297 | host: 298 | type: string 299 | httpHeaders: 300 | items: 301 | properties: 302 | name: 303 | type: string 304 | value: 305 | type: string 306 | required: 307 | - name 308 | - value 309 | type: object 310 | type: array 311 | path: 312 | type: string 313 | port: 314 | anyOf: 315 | - type: integer 316 | - type: string 317 | x-kubernetes-int-or-string: true 318 | scheme: 319 | type: string 320 | required: 321 | - port 322 | type: object 323 | tcpSocket: 324 | properties: 325 | host: 326 | type: string 327 | port: 328 | anyOf: 329 | - type: integer 330 | - type: string 331 | x-kubernetes-int-or-string: true 332 | required: 333 | - port 334 | type: object 335 | type: object 336 | type: object 337 | livenessProbe: 338 | properties: 339 | exec: 340 | properties: 341 | command: 342 | items: 343 | type: string 344 | type: array 345 | type: object 346 | failureThreshold: 347 | format: int32 348 | type: integer 349 | grpc: 350 | properties: 351 | port: 352 | format: int32 353 | type: integer 354 | service: 355 | type: string 356 | required: 357 | - port 358 | type: object 359 | httpGet: 360 | properties: 361 | host: 362 | type: string 363 | httpHeaders: 364 | items: 365 | properties: 366 | name: 367 | type: string 368 | value: 369 | type: string 370 | required: 371 | - name 372 | - value 373 | type: object 374 | type: array 375 | path: 376 | type: string 377 | port: 378 | anyOf: 379 | - type: integer 380 | - type: string 381 | x-kubernetes-int-or-string: true 382 | scheme: 383 | type: string 384 | required: 385 | - port 386 | type: object 387 | initialDelaySeconds: 388 | format: int32 389 | type: integer 390 | periodSeconds: 391 | format: int32 392 | type: integer 393 | successThreshold: 394 | format: int32 395 | type: integer 396 | tcpSocket: 397 | properties: 398 | host: 399 | type: string 400 | port: 401 | anyOf: 402 | - type: integer 403 | - type: string 404 | x-kubernetes-int-or-string: true 405 | required: 406 | - port 407 | type: object 408 | terminationGracePeriodSeconds: 409 | format: int64 410 | type: integer 411 | timeoutSeconds: 412 | format: int32 413 | type: integer 414 | type: object 415 | name: 416 | type: string 417 | ports: 418 | items: 419 | properties: 420 | containerPort: 421 | format: int32 422 | type: integer 423 | hostIP: 424 | type: string 425 | hostPort: 426 | format: int32 427 | type: integer 428 | name: 429 | type: string 430 | protocol: 431 | default: TCP 432 | type: string 433 | required: 434 | - containerPort 435 | type: object 436 | type: array 437 | x-kubernetes-list-map-keys: 438 | - containerPort 439 | - protocol 440 | x-kubernetes-list-type: map 441 | readinessProbe: 442 | properties: 443 | exec: 444 | properties: 445 | command: 446 | items: 447 | type: string 448 | type: array 449 | type: object 450 | failureThreshold: 451 | format: int32 452 | type: integer 453 | grpc: 454 | properties: 455 | port: 456 | format: int32 457 | type: integer 458 | service: 459 | type: string 460 | required: 461 | - port 462 | type: object 463 | httpGet: 464 | properties: 465 | host: 466 | type: string 467 | httpHeaders: 468 | items: 469 | properties: 470 | name: 471 | type: string 472 | value: 473 | type: string 474 | required: 475 | - name 476 | - value 477 | type: object 478 | type: array 479 | path: 480 | type: string 481 | port: 482 | anyOf: 483 | - type: integer 484 | - type: string 485 | x-kubernetes-int-or-string: true 486 | scheme: 487 | type: string 488 | required: 489 | - port 490 | type: object 491 | initialDelaySeconds: 492 | format: int32 493 | type: integer 494 | periodSeconds: 495 | format: int32 496 | type: integer 497 | successThreshold: 498 | format: int32 499 | type: integer 500 | tcpSocket: 501 | properties: 502 | host: 503 | type: string 504 | port: 505 | anyOf: 506 | - type: integer 507 | - type: string 508 | x-kubernetes-int-or-string: true 509 | required: 510 | - port 511 | type: object 512 | terminationGracePeriodSeconds: 513 | format: int64 514 | type: integer 515 | timeoutSeconds: 516 | format: int32 517 | type: integer 518 | type: object 519 | resources: 520 | properties: 521 | limits: 522 | additionalProperties: 523 | anyOf: 524 | - type: integer 525 | - type: string 526 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 527 | x-kubernetes-int-or-string: true 528 | type: object 529 | requests: 530 | additionalProperties: 531 | anyOf: 532 | - type: integer 533 | - type: string 534 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 535 | x-kubernetes-int-or-string: true 536 | type: object 537 | type: object 538 | securityContext: 539 | properties: 540 | allowPrivilegeEscalation: 541 | type: boolean 542 | capabilities: 543 | properties: 544 | add: 545 | items: 546 | type: string 547 | type: array 548 | drop: 549 | items: 550 | type: string 551 | type: array 552 | type: object 553 | privileged: 554 | type: boolean 555 | procMount: 556 | type: string 557 | readOnlyRootFilesystem: 558 | type: boolean 559 | runAsGroup: 560 | format: int64 561 | type: integer 562 | runAsNonRoot: 563 | type: boolean 564 | runAsUser: 565 | format: int64 566 | type: integer 567 | seLinuxOptions: 568 | properties: 569 | level: 570 | type: string 571 | role: 572 | type: string 573 | type: 574 | type: string 575 | user: 576 | type: string 577 | type: object 578 | seccompProfile: 579 | properties: 580 | localhostProfile: 581 | type: string 582 | type: 583 | type: string 584 | required: 585 | - type 586 | type: object 587 | windowsOptions: 588 | properties: 589 | gmsaCredentialSpec: 590 | type: string 591 | gmsaCredentialSpecName: 592 | type: string 593 | hostProcess: 594 | type: boolean 595 | runAsUserName: 596 | type: string 597 | type: object 598 | type: object 599 | startupProbe: 600 | properties: 601 | exec: 602 | properties: 603 | command: 604 | items: 605 | type: string 606 | type: array 607 | type: object 608 | failureThreshold: 609 | format: int32 610 | type: integer 611 | grpc: 612 | properties: 613 | port: 614 | format: int32 615 | type: integer 616 | service: 617 | type: string 618 | required: 619 | - port 620 | type: object 621 | httpGet: 622 | properties: 623 | host: 624 | type: string 625 | httpHeaders: 626 | items: 627 | properties: 628 | name: 629 | type: string 630 | value: 631 | type: string 632 | required: 633 | - name 634 | - value 635 | type: object 636 | type: array 637 | path: 638 | type: string 639 | port: 640 | anyOf: 641 | - type: integer 642 | - type: string 643 | x-kubernetes-int-or-string: true 644 | scheme: 645 | type: string 646 | required: 647 | - port 648 | type: object 649 | initialDelaySeconds: 650 | format: int32 651 | type: integer 652 | periodSeconds: 653 | format: int32 654 | type: integer 655 | successThreshold: 656 | format: int32 657 | type: integer 658 | tcpSocket: 659 | properties: 660 | host: 661 | type: string 662 | port: 663 | anyOf: 664 | - type: integer 665 | - type: string 666 | x-kubernetes-int-or-string: true 667 | required: 668 | - port 669 | type: object 670 | terminationGracePeriodSeconds: 671 | format: int64 672 | type: integer 673 | timeoutSeconds: 674 | format: int32 675 | type: integer 676 | type: object 677 | stdin: 678 | type: boolean 679 | stdinOnce: 680 | type: boolean 681 | terminationMessagePath: 682 | type: string 683 | terminationMessagePolicy: 684 | type: string 685 | tty: 686 | type: boolean 687 | volumeDevices: 688 | items: 689 | properties: 690 | devicePath: 691 | type: string 692 | name: 693 | type: string 694 | required: 695 | - devicePath 696 | - name 697 | type: object 698 | type: array 699 | volumeMounts: 700 | items: 701 | properties: 702 | mountPath: 703 | type: string 704 | mountPropagation: 705 | type: string 706 | name: 707 | type: string 708 | readOnly: 709 | type: boolean 710 | subPath: 711 | type: string 712 | subPathExpr: 713 | type: string 714 | required: 715 | - mountPath 716 | - name 717 | type: object 718 | type: array 719 | workingDir: 720 | type: string 721 | required: 722 | - name 723 | type: object 724 | type: array 725 | labels: 726 | additionalProperties: 727 | type: string 728 | type: object 729 | selector: 730 | properties: 731 | matchExpressions: 732 | items: 733 | properties: 734 | key: 735 | type: string 736 | operator: 737 | type: string 738 | values: 739 | items: 740 | type: string 741 | type: array 742 | required: 743 | - key 744 | - operator 745 | type: object 746 | type: array 747 | matchLabels: 748 | additionalProperties: 749 | type: string 750 | type: object 751 | type: object 752 | serviceAccountName: 753 | type: string 754 | sidecars: 755 | items: 756 | properties: 757 | args: 758 | items: 759 | type: string 760 | type: array 761 | command: 762 | items: 763 | type: string 764 | type: array 765 | env: 766 | items: 767 | properties: 768 | name: 769 | type: string 770 | value: 771 | type: string 772 | valueFrom: 773 | properties: 774 | configMapKeyRef: 775 | properties: 776 | key: 777 | type: string 778 | name: 779 | type: string 780 | optional: 781 | type: boolean 782 | required: 783 | - key 784 | type: object 785 | fieldRef: 786 | properties: 787 | apiVersion: 788 | type: string 789 | fieldPath: 790 | type: string 791 | required: 792 | - fieldPath 793 | type: object 794 | resourceFieldRef: 795 | properties: 796 | containerName: 797 | type: string 798 | divisor: 799 | anyOf: 800 | - type: integer 801 | - type: string 802 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 803 | x-kubernetes-int-or-string: true 804 | resource: 805 | type: string 806 | required: 807 | - resource 808 | type: object 809 | secretKeyRef: 810 | properties: 811 | key: 812 | type: string 813 | name: 814 | type: string 815 | optional: 816 | type: boolean 817 | required: 818 | - key 819 | type: object 820 | type: object 821 | required: 822 | - name 823 | type: object 824 | type: array 825 | envFrom: 826 | items: 827 | properties: 828 | configMapRef: 829 | properties: 830 | name: 831 | type: string 832 | optional: 833 | type: boolean 834 | type: object 835 | prefix: 836 | type: string 837 | secretRef: 838 | properties: 839 | name: 840 | type: string 841 | optional: 842 | type: boolean 843 | type: object 844 | type: object 845 | type: array 846 | image: 847 | type: string 848 | imagePullPolicy: 849 | type: string 850 | lifecycle: 851 | properties: 852 | postStart: 853 | properties: 854 | exec: 855 | properties: 856 | command: 857 | items: 858 | type: string 859 | type: array 860 | type: object 861 | httpGet: 862 | properties: 863 | host: 864 | type: string 865 | httpHeaders: 866 | items: 867 | properties: 868 | name: 869 | type: string 870 | value: 871 | type: string 872 | required: 873 | - name 874 | - value 875 | type: object 876 | type: array 877 | path: 878 | type: string 879 | port: 880 | anyOf: 881 | - type: integer 882 | - type: string 883 | x-kubernetes-int-or-string: true 884 | scheme: 885 | type: string 886 | required: 887 | - port 888 | type: object 889 | tcpSocket: 890 | properties: 891 | host: 892 | type: string 893 | port: 894 | anyOf: 895 | - type: integer 896 | - type: string 897 | x-kubernetes-int-or-string: true 898 | required: 899 | - port 900 | type: object 901 | type: object 902 | preStop: 903 | properties: 904 | exec: 905 | properties: 906 | command: 907 | items: 908 | type: string 909 | type: array 910 | type: object 911 | httpGet: 912 | properties: 913 | host: 914 | type: string 915 | httpHeaders: 916 | items: 917 | properties: 918 | name: 919 | type: string 920 | value: 921 | type: string 922 | required: 923 | - name 924 | - value 925 | type: object 926 | type: array 927 | path: 928 | type: string 929 | port: 930 | anyOf: 931 | - type: integer 932 | - type: string 933 | x-kubernetes-int-or-string: true 934 | scheme: 935 | type: string 936 | required: 937 | - port 938 | type: object 939 | tcpSocket: 940 | properties: 941 | host: 942 | type: string 943 | port: 944 | anyOf: 945 | - type: integer 946 | - type: string 947 | x-kubernetes-int-or-string: true 948 | required: 949 | - port 950 | type: object 951 | type: object 952 | type: object 953 | livenessProbe: 954 | properties: 955 | exec: 956 | properties: 957 | command: 958 | items: 959 | type: string 960 | type: array 961 | type: object 962 | failureThreshold: 963 | format: int32 964 | type: integer 965 | grpc: 966 | properties: 967 | port: 968 | format: int32 969 | type: integer 970 | service: 971 | type: string 972 | required: 973 | - port 974 | type: object 975 | httpGet: 976 | properties: 977 | host: 978 | type: string 979 | httpHeaders: 980 | items: 981 | properties: 982 | name: 983 | type: string 984 | value: 985 | type: string 986 | required: 987 | - name 988 | - value 989 | type: object 990 | type: array 991 | path: 992 | type: string 993 | port: 994 | anyOf: 995 | - type: integer 996 | - type: string 997 | x-kubernetes-int-or-string: true 998 | scheme: 999 | type: string 1000 | required: 1001 | - port 1002 | type: object 1003 | initialDelaySeconds: 1004 | format: int32 1005 | type: integer 1006 | periodSeconds: 1007 | format: int32 1008 | type: integer 1009 | successThreshold: 1010 | format: int32 1011 | type: integer 1012 | tcpSocket: 1013 | properties: 1014 | host: 1015 | type: string 1016 | port: 1017 | anyOf: 1018 | - type: integer 1019 | - type: string 1020 | x-kubernetes-int-or-string: true 1021 | required: 1022 | - port 1023 | type: object 1024 | terminationGracePeriodSeconds: 1025 | format: int64 1026 | type: integer 1027 | timeoutSeconds: 1028 | format: int32 1029 | type: integer 1030 | type: object 1031 | name: 1032 | type: string 1033 | ports: 1034 | items: 1035 | properties: 1036 | containerPort: 1037 | format: int32 1038 | type: integer 1039 | hostIP: 1040 | type: string 1041 | hostPort: 1042 | format: int32 1043 | type: integer 1044 | name: 1045 | type: string 1046 | protocol: 1047 | default: TCP 1048 | type: string 1049 | required: 1050 | - containerPort 1051 | type: object 1052 | type: array 1053 | x-kubernetes-list-map-keys: 1054 | - containerPort 1055 | - protocol 1056 | x-kubernetes-list-type: map 1057 | readinessProbe: 1058 | properties: 1059 | exec: 1060 | properties: 1061 | command: 1062 | items: 1063 | type: string 1064 | type: array 1065 | type: object 1066 | failureThreshold: 1067 | format: int32 1068 | type: integer 1069 | grpc: 1070 | properties: 1071 | port: 1072 | format: int32 1073 | type: integer 1074 | service: 1075 | type: string 1076 | required: 1077 | - port 1078 | type: object 1079 | httpGet: 1080 | properties: 1081 | host: 1082 | type: string 1083 | httpHeaders: 1084 | items: 1085 | properties: 1086 | name: 1087 | type: string 1088 | value: 1089 | type: string 1090 | required: 1091 | - name 1092 | - value 1093 | type: object 1094 | type: array 1095 | path: 1096 | type: string 1097 | port: 1098 | anyOf: 1099 | - type: integer 1100 | - type: string 1101 | x-kubernetes-int-or-string: true 1102 | scheme: 1103 | type: string 1104 | required: 1105 | - port 1106 | type: object 1107 | initialDelaySeconds: 1108 | format: int32 1109 | type: integer 1110 | periodSeconds: 1111 | format: int32 1112 | type: integer 1113 | successThreshold: 1114 | format: int32 1115 | type: integer 1116 | tcpSocket: 1117 | properties: 1118 | host: 1119 | type: string 1120 | port: 1121 | anyOf: 1122 | - type: integer 1123 | - type: string 1124 | x-kubernetes-int-or-string: true 1125 | required: 1126 | - port 1127 | type: object 1128 | terminationGracePeriodSeconds: 1129 | format: int64 1130 | type: integer 1131 | timeoutSeconds: 1132 | format: int32 1133 | type: integer 1134 | type: object 1135 | resources: 1136 | properties: 1137 | limits: 1138 | additionalProperties: 1139 | anyOf: 1140 | - type: integer 1141 | - type: string 1142 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1143 | x-kubernetes-int-or-string: true 1144 | type: object 1145 | requests: 1146 | additionalProperties: 1147 | anyOf: 1148 | - type: integer 1149 | - type: string 1150 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1151 | x-kubernetes-int-or-string: true 1152 | type: object 1153 | type: object 1154 | securityContext: 1155 | properties: 1156 | allowPrivilegeEscalation: 1157 | type: boolean 1158 | capabilities: 1159 | properties: 1160 | add: 1161 | items: 1162 | type: string 1163 | type: array 1164 | drop: 1165 | items: 1166 | type: string 1167 | type: array 1168 | type: object 1169 | privileged: 1170 | type: boolean 1171 | procMount: 1172 | type: string 1173 | readOnlyRootFilesystem: 1174 | type: boolean 1175 | runAsGroup: 1176 | format: int64 1177 | type: integer 1178 | runAsNonRoot: 1179 | type: boolean 1180 | runAsUser: 1181 | format: int64 1182 | type: integer 1183 | seLinuxOptions: 1184 | properties: 1185 | level: 1186 | type: string 1187 | role: 1188 | type: string 1189 | type: 1190 | type: string 1191 | user: 1192 | type: string 1193 | type: object 1194 | seccompProfile: 1195 | properties: 1196 | localhostProfile: 1197 | type: string 1198 | type: 1199 | type: string 1200 | required: 1201 | - type 1202 | type: object 1203 | windowsOptions: 1204 | properties: 1205 | gmsaCredentialSpec: 1206 | type: string 1207 | gmsaCredentialSpecName: 1208 | type: string 1209 | hostProcess: 1210 | type: boolean 1211 | runAsUserName: 1212 | type: string 1213 | type: object 1214 | type: object 1215 | startupProbe: 1216 | properties: 1217 | exec: 1218 | properties: 1219 | command: 1220 | items: 1221 | type: string 1222 | type: array 1223 | type: object 1224 | failureThreshold: 1225 | format: int32 1226 | type: integer 1227 | grpc: 1228 | properties: 1229 | port: 1230 | format: int32 1231 | type: integer 1232 | service: 1233 | type: string 1234 | required: 1235 | - port 1236 | type: object 1237 | httpGet: 1238 | properties: 1239 | host: 1240 | type: string 1241 | httpHeaders: 1242 | items: 1243 | properties: 1244 | name: 1245 | type: string 1246 | value: 1247 | type: string 1248 | required: 1249 | - name 1250 | - value 1251 | type: object 1252 | type: array 1253 | path: 1254 | type: string 1255 | port: 1256 | anyOf: 1257 | - type: integer 1258 | - type: string 1259 | x-kubernetes-int-or-string: true 1260 | scheme: 1261 | type: string 1262 | required: 1263 | - port 1264 | type: object 1265 | initialDelaySeconds: 1266 | format: int32 1267 | type: integer 1268 | periodSeconds: 1269 | format: int32 1270 | type: integer 1271 | successThreshold: 1272 | format: int32 1273 | type: integer 1274 | tcpSocket: 1275 | properties: 1276 | host: 1277 | type: string 1278 | port: 1279 | anyOf: 1280 | - type: integer 1281 | - type: string 1282 | x-kubernetes-int-or-string: true 1283 | required: 1284 | - port 1285 | type: object 1286 | terminationGracePeriodSeconds: 1287 | format: int64 1288 | type: integer 1289 | timeoutSeconds: 1290 | format: int32 1291 | type: integer 1292 | type: object 1293 | stdin: 1294 | type: boolean 1295 | stdinOnce: 1296 | type: boolean 1297 | terminationMessagePath: 1298 | type: string 1299 | terminationMessagePolicy: 1300 | type: string 1301 | tty: 1302 | type: boolean 1303 | volumeDevices: 1304 | items: 1305 | properties: 1306 | devicePath: 1307 | type: string 1308 | name: 1309 | type: string 1310 | required: 1311 | - devicePath 1312 | - name 1313 | type: object 1314 | type: array 1315 | volumeMounts: 1316 | items: 1317 | properties: 1318 | mountPath: 1319 | type: string 1320 | mountPropagation: 1321 | type: string 1322 | name: 1323 | type: string 1324 | readOnly: 1325 | type: boolean 1326 | subPath: 1327 | type: string 1328 | subPathExpr: 1329 | type: string 1330 | required: 1331 | - mountPath 1332 | - name 1333 | type: object 1334 | type: array 1335 | workingDir: 1336 | type: string 1337 | required: 1338 | - name 1339 | type: object 1340 | type: array 1341 | tolerations: 1342 | items: 1343 | properties: 1344 | effect: 1345 | type: string 1346 | key: 1347 | type: string 1348 | operator: 1349 | type: string 1350 | tolerationSeconds: 1351 | format: int64 1352 | type: integer 1353 | value: 1354 | type: string 1355 | type: object 1356 | type: array 1357 | volumeMounts: 1358 | items: 1359 | properties: 1360 | mountPath: 1361 | type: string 1362 | mountPropagation: 1363 | type: string 1364 | name: 1365 | type: string 1366 | readOnly: 1367 | type: boolean 1368 | subPath: 1369 | type: string 1370 | subPathExpr: 1371 | type: string 1372 | required: 1373 | - mountPath 1374 | - name 1375 | type: object 1376 | type: array 1377 | volumes: 1378 | items: 1379 | properties: 1380 | awsElasticBlockStore: 1381 | properties: 1382 | fsType: 1383 | type: string 1384 | partition: 1385 | format: int32 1386 | type: integer 1387 | readOnly: 1388 | type: boolean 1389 | volumeID: 1390 | type: string 1391 | required: 1392 | - volumeID 1393 | type: object 1394 | azureDisk: 1395 | properties: 1396 | cachingMode: 1397 | type: string 1398 | diskName: 1399 | type: string 1400 | diskURI: 1401 | type: string 1402 | fsType: 1403 | type: string 1404 | kind: 1405 | type: string 1406 | readOnly: 1407 | type: boolean 1408 | required: 1409 | - diskName 1410 | - diskURI 1411 | type: object 1412 | azureFile: 1413 | properties: 1414 | readOnly: 1415 | type: boolean 1416 | secretName: 1417 | type: string 1418 | shareName: 1419 | type: string 1420 | required: 1421 | - secretName 1422 | - shareName 1423 | type: object 1424 | cephfs: 1425 | properties: 1426 | monitors: 1427 | items: 1428 | type: string 1429 | type: array 1430 | path: 1431 | type: string 1432 | readOnly: 1433 | type: boolean 1434 | secretFile: 1435 | type: string 1436 | secretRef: 1437 | properties: 1438 | name: 1439 | type: string 1440 | type: object 1441 | user: 1442 | type: string 1443 | required: 1444 | - monitors 1445 | type: object 1446 | cinder: 1447 | properties: 1448 | fsType: 1449 | type: string 1450 | readOnly: 1451 | type: boolean 1452 | secretRef: 1453 | properties: 1454 | name: 1455 | type: string 1456 | type: object 1457 | volumeID: 1458 | type: string 1459 | required: 1460 | - volumeID 1461 | type: object 1462 | configMap: 1463 | properties: 1464 | defaultMode: 1465 | format: int32 1466 | type: integer 1467 | items: 1468 | items: 1469 | properties: 1470 | key: 1471 | type: string 1472 | mode: 1473 | format: int32 1474 | type: integer 1475 | path: 1476 | type: string 1477 | required: 1478 | - key 1479 | - path 1480 | type: object 1481 | type: array 1482 | name: 1483 | type: string 1484 | optional: 1485 | type: boolean 1486 | type: object 1487 | csi: 1488 | properties: 1489 | driver: 1490 | type: string 1491 | fsType: 1492 | type: string 1493 | nodePublishSecretRef: 1494 | properties: 1495 | name: 1496 | type: string 1497 | type: object 1498 | readOnly: 1499 | type: boolean 1500 | volumeAttributes: 1501 | additionalProperties: 1502 | type: string 1503 | type: object 1504 | required: 1505 | - driver 1506 | type: object 1507 | downwardAPI: 1508 | properties: 1509 | defaultMode: 1510 | format: int32 1511 | type: integer 1512 | items: 1513 | items: 1514 | properties: 1515 | fieldRef: 1516 | properties: 1517 | apiVersion: 1518 | type: string 1519 | fieldPath: 1520 | type: string 1521 | required: 1522 | - fieldPath 1523 | type: object 1524 | mode: 1525 | format: int32 1526 | type: integer 1527 | path: 1528 | type: string 1529 | resourceFieldRef: 1530 | properties: 1531 | containerName: 1532 | type: string 1533 | divisor: 1534 | anyOf: 1535 | - type: integer 1536 | - type: string 1537 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1538 | x-kubernetes-int-or-string: true 1539 | resource: 1540 | type: string 1541 | required: 1542 | - resource 1543 | type: object 1544 | required: 1545 | - path 1546 | type: object 1547 | type: array 1548 | type: object 1549 | emptyDir: 1550 | properties: 1551 | medium: 1552 | type: string 1553 | sizeLimit: 1554 | anyOf: 1555 | - type: integer 1556 | - type: string 1557 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1558 | x-kubernetes-int-or-string: true 1559 | type: object 1560 | ephemeral: 1561 | properties: 1562 | volumeClaimTemplate: 1563 | properties: 1564 | metadata: 1565 | type: object 1566 | spec: 1567 | properties: 1568 | accessModes: 1569 | items: 1570 | type: string 1571 | type: array 1572 | dataSource: 1573 | properties: 1574 | apiGroup: 1575 | type: string 1576 | kind: 1577 | type: string 1578 | name: 1579 | type: string 1580 | required: 1581 | - kind 1582 | - name 1583 | type: object 1584 | dataSourceRef: 1585 | properties: 1586 | apiGroup: 1587 | type: string 1588 | kind: 1589 | type: string 1590 | name: 1591 | type: string 1592 | required: 1593 | - kind 1594 | - name 1595 | type: object 1596 | resources: 1597 | properties: 1598 | limits: 1599 | additionalProperties: 1600 | anyOf: 1601 | - type: integer 1602 | - type: string 1603 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1604 | x-kubernetes-int-or-string: true 1605 | type: object 1606 | requests: 1607 | additionalProperties: 1608 | anyOf: 1609 | - type: integer 1610 | - type: string 1611 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1612 | x-kubernetes-int-or-string: true 1613 | type: object 1614 | type: object 1615 | selector: 1616 | properties: 1617 | matchExpressions: 1618 | items: 1619 | properties: 1620 | key: 1621 | type: string 1622 | operator: 1623 | type: string 1624 | values: 1625 | items: 1626 | type: string 1627 | type: array 1628 | required: 1629 | - key 1630 | - operator 1631 | type: object 1632 | type: array 1633 | matchLabels: 1634 | additionalProperties: 1635 | type: string 1636 | type: object 1637 | type: object 1638 | storageClassName: 1639 | type: string 1640 | volumeMode: 1641 | type: string 1642 | volumeName: 1643 | type: string 1644 | type: object 1645 | required: 1646 | - spec 1647 | type: object 1648 | type: object 1649 | fc: 1650 | properties: 1651 | fsType: 1652 | type: string 1653 | lun: 1654 | format: int32 1655 | type: integer 1656 | readOnly: 1657 | type: boolean 1658 | targetWWNs: 1659 | items: 1660 | type: string 1661 | type: array 1662 | wwids: 1663 | items: 1664 | type: string 1665 | type: array 1666 | type: object 1667 | flexVolume: 1668 | properties: 1669 | driver: 1670 | type: string 1671 | fsType: 1672 | type: string 1673 | options: 1674 | additionalProperties: 1675 | type: string 1676 | type: object 1677 | readOnly: 1678 | type: boolean 1679 | secretRef: 1680 | properties: 1681 | name: 1682 | type: string 1683 | type: object 1684 | required: 1685 | - driver 1686 | type: object 1687 | flocker: 1688 | properties: 1689 | datasetName: 1690 | type: string 1691 | datasetUUID: 1692 | type: string 1693 | type: object 1694 | gcePersistentDisk: 1695 | properties: 1696 | fsType: 1697 | type: string 1698 | partition: 1699 | format: int32 1700 | type: integer 1701 | pdName: 1702 | type: string 1703 | readOnly: 1704 | type: boolean 1705 | required: 1706 | - pdName 1707 | type: object 1708 | gitRepo: 1709 | properties: 1710 | directory: 1711 | type: string 1712 | repository: 1713 | type: string 1714 | revision: 1715 | type: string 1716 | required: 1717 | - repository 1718 | type: object 1719 | glusterfs: 1720 | properties: 1721 | endpoints: 1722 | type: string 1723 | path: 1724 | type: string 1725 | readOnly: 1726 | type: boolean 1727 | required: 1728 | - endpoints 1729 | - path 1730 | type: object 1731 | hostPath: 1732 | properties: 1733 | path: 1734 | type: string 1735 | type: 1736 | type: string 1737 | required: 1738 | - path 1739 | type: object 1740 | iscsi: 1741 | properties: 1742 | chapAuthDiscovery: 1743 | type: boolean 1744 | chapAuthSession: 1745 | type: boolean 1746 | fsType: 1747 | type: string 1748 | initiatorName: 1749 | type: string 1750 | iqn: 1751 | type: string 1752 | iscsiInterface: 1753 | type: string 1754 | lun: 1755 | format: int32 1756 | type: integer 1757 | portals: 1758 | items: 1759 | type: string 1760 | type: array 1761 | readOnly: 1762 | type: boolean 1763 | secretRef: 1764 | properties: 1765 | name: 1766 | type: string 1767 | type: object 1768 | targetPortal: 1769 | type: string 1770 | required: 1771 | - iqn 1772 | - lun 1773 | - targetPortal 1774 | type: object 1775 | name: 1776 | type: string 1777 | nfs: 1778 | properties: 1779 | path: 1780 | type: string 1781 | readOnly: 1782 | type: boolean 1783 | server: 1784 | type: string 1785 | required: 1786 | - path 1787 | - server 1788 | type: object 1789 | persistentVolumeClaim: 1790 | properties: 1791 | claimName: 1792 | type: string 1793 | readOnly: 1794 | type: boolean 1795 | required: 1796 | - claimName 1797 | type: object 1798 | photonPersistentDisk: 1799 | properties: 1800 | fsType: 1801 | type: string 1802 | pdID: 1803 | type: string 1804 | required: 1805 | - pdID 1806 | type: object 1807 | portworxVolume: 1808 | properties: 1809 | fsType: 1810 | type: string 1811 | readOnly: 1812 | type: boolean 1813 | volumeID: 1814 | type: string 1815 | required: 1816 | - volumeID 1817 | type: object 1818 | projected: 1819 | properties: 1820 | defaultMode: 1821 | format: int32 1822 | type: integer 1823 | sources: 1824 | items: 1825 | properties: 1826 | configMap: 1827 | properties: 1828 | items: 1829 | items: 1830 | properties: 1831 | key: 1832 | type: string 1833 | mode: 1834 | format: int32 1835 | type: integer 1836 | path: 1837 | type: string 1838 | required: 1839 | - key 1840 | - path 1841 | type: object 1842 | type: array 1843 | name: 1844 | type: string 1845 | optional: 1846 | type: boolean 1847 | type: object 1848 | downwardAPI: 1849 | properties: 1850 | items: 1851 | items: 1852 | properties: 1853 | fieldRef: 1854 | properties: 1855 | apiVersion: 1856 | type: string 1857 | fieldPath: 1858 | type: string 1859 | required: 1860 | - fieldPath 1861 | type: object 1862 | mode: 1863 | format: int32 1864 | type: integer 1865 | path: 1866 | type: string 1867 | resourceFieldRef: 1868 | properties: 1869 | containerName: 1870 | type: string 1871 | divisor: 1872 | anyOf: 1873 | - type: integer 1874 | - type: string 1875 | pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1876 | x-kubernetes-int-or-string: true 1877 | resource: 1878 | type: string 1879 | required: 1880 | - resource 1881 | type: object 1882 | required: 1883 | - path 1884 | type: object 1885 | type: array 1886 | type: object 1887 | secret: 1888 | properties: 1889 | items: 1890 | items: 1891 | properties: 1892 | key: 1893 | type: string 1894 | mode: 1895 | format: int32 1896 | type: integer 1897 | path: 1898 | type: string 1899 | required: 1900 | - key 1901 | - path 1902 | type: object 1903 | type: array 1904 | name: 1905 | type: string 1906 | optional: 1907 | type: boolean 1908 | type: object 1909 | serviceAccountToken: 1910 | properties: 1911 | audience: 1912 | type: string 1913 | expirationSeconds: 1914 | format: int64 1915 | type: integer 1916 | path: 1917 | type: string 1918 | required: 1919 | - path 1920 | type: object 1921 | type: object 1922 | type: array 1923 | type: object 1924 | quobyte: 1925 | properties: 1926 | group: 1927 | type: string 1928 | readOnly: 1929 | type: boolean 1930 | registry: 1931 | type: string 1932 | tenant: 1933 | type: string 1934 | user: 1935 | type: string 1936 | volume: 1937 | type: string 1938 | required: 1939 | - registry 1940 | - volume 1941 | type: object 1942 | rbd: 1943 | properties: 1944 | fsType: 1945 | type: string 1946 | image: 1947 | type: string 1948 | keyring: 1949 | type: string 1950 | monitors: 1951 | items: 1952 | type: string 1953 | type: array 1954 | pool: 1955 | type: string 1956 | readOnly: 1957 | type: boolean 1958 | secretRef: 1959 | properties: 1960 | name: 1961 | type: string 1962 | type: object 1963 | user: 1964 | type: string 1965 | required: 1966 | - image 1967 | - monitors 1968 | type: object 1969 | scaleIO: 1970 | properties: 1971 | fsType: 1972 | type: string 1973 | gateway: 1974 | type: string 1975 | protectionDomain: 1976 | type: string 1977 | readOnly: 1978 | type: boolean 1979 | secretRef: 1980 | properties: 1981 | name: 1982 | type: string 1983 | type: object 1984 | sslEnabled: 1985 | type: boolean 1986 | storageMode: 1987 | type: string 1988 | storagePool: 1989 | type: string 1990 | system: 1991 | type: string 1992 | volumeName: 1993 | type: string 1994 | required: 1995 | - gateway 1996 | - secretRef 1997 | - system 1998 | type: object 1999 | secret: 2000 | properties: 2001 | defaultMode: 2002 | format: int32 2003 | type: integer 2004 | items: 2005 | items: 2006 | properties: 2007 | key: 2008 | type: string 2009 | mode: 2010 | format: int32 2011 | type: integer 2012 | path: 2013 | type: string 2014 | required: 2015 | - key 2016 | - path 2017 | type: object 2018 | type: array 2019 | optional: 2020 | type: boolean 2021 | secretName: 2022 | type: string 2023 | type: object 2024 | storageos: 2025 | properties: 2026 | fsType: 2027 | type: string 2028 | readOnly: 2029 | type: boolean 2030 | secretRef: 2031 | properties: 2032 | name: 2033 | type: string 2034 | type: object 2035 | volumeName: 2036 | type: string 2037 | volumeNamespace: 2038 | type: string 2039 | type: object 2040 | vsphereVolume: 2041 | properties: 2042 | fsType: 2043 | type: string 2044 | storagePolicyID: 2045 | type: string 2046 | storagePolicyName: 2047 | type: string 2048 | volumePath: 2049 | type: string 2050 | required: 2051 | - volumePath 2052 | type: object 2053 | required: 2054 | - name 2055 | type: object 2056 | type: array 2057 | required: 2058 | - selector 2059 | type: object 2060 | status: 2061 | type: object 2062 | type: object 2063 | served: true 2064 | storage: true 2065 | status: 2066 | acceptedNames: 2067 | kind: "" 2068 | plural: "" 2069 | conditions: [] 2070 | storedVersions: [] 2071 | --------------------------------------------------------------------------------