├── .github └── CODEOWNERS ├── .gitignore ├── LICENSE ├── README.md ├── deployment ├── configmap.yaml ├── deployment.yaml ├── mutatingwebhook-ca-bundle.yaml ├── mutatingwebhook.yaml ├── service.yaml ├── test_deployment.yaml ├── webhook-create-signed-cert.sh └── webhook-patch-ca-bundle.sh ├── env-injector-azure-pipelines-image-publish.yaml ├── env-injector-azure-pipelines.yaml ├── env-injector-webhook ├── .helmignore ├── Chart.yaml ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── env-configmap.yaml │ ├── mutatingwebhook.yaml │ ├── post-delete-configmap.yaml │ ├── post-delete-job.yaml │ ├── pre-install-configmap.yaml │ ├── pre-install-job.yaml │ ├── rbac.yaml │ └── service.yaml ├── test-pod.yaml └── values.yaml ├── image ├── Dockerfile ├── add_affinities.go ├── add_dns_options.go ├── add_env_var.go ├── add_tolerations.go ├── add_topology_constraints.go ├── create_patch.go ├── go.mod ├── go.sum ├── helper_functions.go ├── main.go ├── remove_pod_affinity.go ├── test │ ├── env_test_1.yaml │ ├── env_test_2.yaml │ ├── env_test_3.yaml │ ├── env_test_4.yaml │ ├── env_test_5.yaml │ ├── env_test_6.yaml │ └── env_test_7.yaml ├── webhook.go └── webhook_test.go ├── k8s-env-injector-build.yaml └── renovate.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hmcts/platform-operations 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Application binary 8 | k8s-env-injector 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool 14 | *.out 15 | 16 | vendor/* 17 | 18 | .idea 19 | .DS_store 20 | *.iml 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 HM Courts & Tribunals Service 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Mutating Admission Webhook for environment injection 2 | 3 | This repo hosts a [MutatingAdmissionWebhook](https://kubernetes.io/docs/admin/admission-controllers/#mutatingadmissionwebhook-beta-in-19) that injects environment variables, dns options and node affinity into pod containers prior to persistence of the object. 4 | Node affinity is currently limited to RequiredDuringSchedulingIgnoredDuringExecution selector terms. 5 | 6 | The image can be used along with several Kubernetes resources to update specific settings on your pods based on a label applied to the namespace of the pod i.e. 7 | 8 | - Apply the label to the namespace and all pods within will have the same additional configuration applied at scheduling time. 9 | 10 | The following options are available for configuration (if existing configuration exists then the new configuration you supply will be appended, it does not replace existing configuration). 11 | 12 | - Environment Variables 13 | - DNS Options 14 | - Required Node Affinity terms 15 | - Preferred Node Affinity terms 16 | - Tolerations 17 | - Topology Spread Constraints 18 | 19 | Each configuration type is optional so your configmap or values file will only include those that you want to change. 20 | 21 | Example config map: 22 | 23 | ```yaml 24 | apiVersion: v1 25 | kind: ConfigMap 26 | metadata: 27 | name: env-injector-webhook-configmap 28 | data: 29 | envconfig.yaml: | 30 | tolerations: 31 | - key: kubernetes.azure.com/scalesetpriority 32 | effect: NoSchedule 33 | operator: Equal 34 | value: spot 35 | topologyConstraints: 36 | - maxSkew: 1 37 | topologyKey: topology.kubernetes.io/zone 38 | whenUnsatisfiable: ScheduleAnyway 39 | nodeAffinityPolicy: Honor 40 | nodeTaintsPolicy: Honor 41 | labelSelector: 42 | matchLabels: 43 | app.kubernetes.io/name: test-app 44 | matchLabelKeys: 45 | - pod-template-hash 46 | ``` 47 | 48 | example values.yaml file (Helm): 49 | 50 | ```yaml 51 | environment: {} 52 | dnsOptions: {} 53 | requiredNodeAffinityTerms: {} 54 | preferredNodeAffinityTerms: {} 55 | tolerations: 56 | - key: kubernetes.azure.com/scalesetpriority 57 | effect: NoSchedule 58 | operator: Equal 59 | value: spot 60 | topologyConstraints: 61 | - maxSkew: 1 62 | topologyKey: topology.kubernetes.io/zone 63 | whenUnsatisfiable: ScheduleAnyway 64 | nodeAffinityPolicy: Honor 65 | nodeTaintsPolicy: Honor 66 | labelSelector: 67 | matchLabels: 68 | app.kubernetes.io/name: test-app 69 | matchLabelKeys: 70 | - pod-template-hash 71 | 72 | ``` 73 | 74 | ## Prerequisites 75 | 76 | Kubernetes 1.22.0 or above with the `admissionregistration.k8s.io/v1` API enabled. Verify that by the following command: 77 | ``` 78 | kubectl api-versions | grep admissionregistration.k8s.io/v1 79 | ``` 80 | The result should be: 81 | ``` 82 | admissionregistration.k8s.io/v1 83 | ``` 84 | 85 | In addition, the `MutatingAdmissionWebhook` and `ValidatingAdmissionWebhook` admission controllers should be added and listed in the correct order in the admission-control flag of kube-apiserver. 86 | 87 | ## Build 88 | Within this repository is a Dockerfile, this should be used when there are changes made to the Golang code. 89 | 90 | Build and push docker image 91 | 92 | ``` 93 | docker build --tag k8s-env-injector:latest . 94 | 95 | docker tag k8s-env-injector /k8s-env-injector: 96 | 97 | docker push /k8s-env-injector: 98 | 99 | ``` 100 | 101 | ## Deploy 102 | 103 | Create a signed cert/key pair and store it in a Kubernetes `secret` that will be consumed by env-injector deployment 104 | 105 | ``` 106 | ./deployment/webhook-create-signed-cert.sh \ 107 | --service env-injector-webhook-svc \ 108 | --secret env-injector-webhook-certs \ 109 | --namespace default 110 | ``` 111 | 112 | > **_NOTE:_** This creates a secret within your namespace so you need to use this namespace for the rest of the deployment steps 113 | 114 | Patch the `MutatingWebhookConfiguration` by set `caBundle` with correct value from Kubernetes cluster 115 | 116 | ``` 117 | cat deployment/mutatingwebhook.yaml | \ 118 | deployment/webhook-patch-ca-bundle.sh > \ 119 | deployment/mutatingwebhook-ca-bundle.yaml 120 | ``` 121 | 122 | This will update the local `deployment/mutatingwebhook-ca-bundle.yaml` file with a new CA bundle string, make sure to check that it also has the matching namespace file before you deploy. 123 | 124 | Deploy resources 125 | 126 | ``` 127 | kubectl create -f deployment/configmap.yaml 128 | kubectl create -f deployment/deployment.yaml 129 | kubectl create -f deployment/service.yaml 130 | kubectl create -f deployment/mutatingwebhook-ca-bundle.yaml 131 | ``` 132 | 133 | ## Verify 134 | 135 | The environment inject webhook should be running now in your namespace, you can verify by: 136 | 137 | ``` 138 | kubectl get pods 139 | NAME READY STATUS RESTARTS AGE 140 | env-injector-webhook-deployment-bbb689d69-882dd 1/1 Running 0 5m 141 | ``` 142 | 143 | ``` 144 | kubectl get deployment 145 | NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE 146 | env-injector-webhook-deployment 1 1 1 1 5m 147 | ``` 148 | 149 | Add a label to the namespace that you want env-injector to make changes to with: `env-injector=enabled` 150 | 151 | ``` 152 | kubectl label namespace default hmcts.github.com/envInjector=enabled 153 | kubectl get namespace -L hmcts.github.com/envInjector 154 | ``` 155 | 156 | Output: 157 | 158 | ``` 159 | NAME STATUS AGE ENVINJECTOR 160 | default Active 4d3h enabled 161 | kube-node-lease Active 4d3h 162 | kube-public Active 4d3h 163 | kube-system Active 4d3h 164 | ``` 165 | 166 | Create a test deployment in your namespace, the following is an example that can be used for quick results: 167 | 168 | ``` 169 | cat < 213 | ``` 214 | 215 | *Note*: As the pods and service need to have: 216 | - a secret containing a signed certificate and key 217 | - a mutating webhook patched with the CA Bundle 218 | the script executed from `pre-install-job.yaml` takes care of creating them executing as a helm pre-install + pre-upgrade hook. 219 | This allows the installation/upgrade steps to execute in the right order, but has the (unfortunate) side effect of leaving 220 | around the secret and mutating webhook when the chart is deleted. 221 | For that reason a pre-upgrade + post-delete helm hook takes care of deleting secret and admission webhook. 222 | 223 | ## Updates 224 | If you wish to update or increase the coverage of this webhook you can use the following API Guide for Kubernetes and Golang: 225 | 226 | - https://pkg.go.dev/k8s.io/api/core/v1 227 | 228 | ## Notes 229 | 230 | This repo is based on the excellent tutorial available at: [morvencao/kube-mutating-webhook-tutorial](https://github.com/morvencao/kube-mutating-webhook-tutorial) 231 | -------------------------------------------------------------------------------- /deployment/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: env-injector-webhook-configmap 5 | data: 6 | envconfig.yaml: | 7 | env: 8 | - name: INJECTOR_TEST 9 | value: enabled 10 | removePodAntiAffinity: true 11 | requiredNodeAffinityTerms: 12 | - matchExpressions: 13 | - key: kubernetes.azure.com/mode 14 | operator: NotIn 15 | values: 16 | - system 17 | - key: kubernetes.azure.com/scalesetpriority 18 | operator: DoesNotExist 19 | preferredNodeAffinityTerms: 20 | - weight: 50 21 | preference: 22 | matchExpressions: 23 | - key: kubernetes.azure.com/scalesetpriority 24 | operator: In 25 | values: 26 | - spot 27 | - weight: 1 28 | preference: 29 | matchExpressions: 30 | - key: kubernetes.azure.com/scalesetpriority 31 | operator: DoesNotExist 32 | tolerations: 33 | - key: kubernetes.azure.com/scalesetpriority 34 | effect: NoSchedule 35 | operator: Equal 36 | value: spot 37 | topologyConstraints: 38 | - maxSkew: 1 39 | topologyKey: kubernetes.azure.com/agentpool 40 | whenUnsatisfiable: DoNotSchedule 41 | nodeAffinityPolicy: Honor 42 | nodeTaintsPolicy: Honor 43 | labelSelector: 44 | matchLabels: 45 | app.kubernetes.io/managed-by: Helm 46 | matchLabelKeys: 47 | - pod-template-hash 48 | - maxSkew: 1 49 | topologyKey: topology.kubernetes.io/zone 50 | whenUnsatisfiable: ScheduleAnyway 51 | nodeAffinityPolicy: Honor 52 | nodeTaintsPolicy: Honor 53 | labelSelector: 54 | matchLabels: 55 | app.kubernetes.io/managed-by: Helm 56 | matchLabelKeys: 57 | - pod-template-hash -------------------------------------------------------------------------------- /deployment/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: env-injector-webhook-deployment 5 | labels: 6 | app: env-injector 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: env-injector 12 | template: 13 | metadata: 14 | labels: 15 | app: env-injector 16 | spec: 17 | containers: 18 | - name: env-injector 19 | image: hmctspublic.azurecr.io/hmcts/k8s-env-injector:492855_20231212 20 | imagePullPolicy: Always 21 | args: 22 | - -envCfgFile=/etc/webhook/config/envconfig.yaml 23 | - -tlsCertFile=/etc/webhook/certs/cert.pem 24 | - -tlsKeyFile=/etc/webhook/certs/key.pem 25 | - -alsologtostderr 26 | - -v=4 27 | - 2>&1 28 | volumeMounts: 29 | - name: webhook-certs 30 | mountPath: /etc/webhook/certs 31 | readOnly: true 32 | - name: webhook-config 33 | mountPath: /etc/webhook/config 34 | volumes: 35 | - name: webhook-certs 36 | secret: 37 | secretName: env-injector-webhook-certs 38 | - name: webhook-config 39 | configMap: 40 | name: env-injector-webhook-configmap 41 | priorityClassName: system-cluster-critical 42 | -------------------------------------------------------------------------------- /deployment/mutatingwebhook-ca-bundle.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: env-injector-webhook-cfg 5 | labels: 6 | app: env-injector 7 | webhooks: 8 | - name: env-injector.hmcts.net 9 | admissionReviewVersions: [v1beta1, v1] 10 | sideEffects: NoneOnDryRun 11 | clientConfig: 12 | service: 13 | name: env-injector-webhook-svc 14 | namespace: default 15 | path: "/mutate" 16 | caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwdGFXNXAKYTNWaVpVTkJNQjRYRFRFNE1EVXhNVEUwTWpNME1sb1hEVEk0TURVd09ERTBNak0wTWxvd0ZURVRNQkVHQTFVRQpBeE1LYldsdWFXdDFZbVZEUVRDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTU1mCnFiVlJqWGJVTGRUZUlZeEt0NnkrUTdhZGhkRDl1bXBvczh3WkJWNTh5TnQwRGE4SC9zQUs1dmxkaG5vMjZMMG4KYkhrU0xDTVNuZmhwSVV6OFkrL2R3MmhwdVhqWWhLc05sMDZ4TlNpenNtamlBTU8vQnJrMlpuSm9BY3lwNEgrRgpNZmZvc3ltcU93NU1VUWRpQXVQRy9qczdWRjFIWmhjUC9oUFVPU1NjSVd6eTUzdHc2czUySFdBYmdNaElJa2RYCjhMUFd1dWZVUFd2UExIYXBEd3FaRnp4ajJhQmRhWEVuVk9yTFliTXp3cERKNXR3MTgxencyUDRmRS83QzM0ZDYKWUVIMVdaQlRMUnBHLzA3anVjUUJ3aUQxNXRLMytnT0FlaHJUS3hjMjJ1Mm5rcVRlbk82TWwxQVZhRnUxUXNXWgpoNlg4R0RNUEVUMTRqSFUycE1jQ0F3RUFBYU5DTUVBd0RnWURWUjBQQVFIL0JBUURBZ0trTUIwR0ExVWRKUVFXCk1CUUdDQ3NHQVFVRkJ3TUNCZ2dyQmdFRkJRY0RBVEFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJYjMKRFFFQkN3VUFBNElCQVFBN2NTcnpIaG1jQVVkL3pNam42UTljV1JqQmxjWm5uSml5NTF4dldCem4zWEZIK3dnVQpJdUFLbzFpeFpBQ3Yxb3ZCb1FraUwxSXA2eEYwcUh0VFVwOTkrMEppSHZXSngxeExkUlBNdEtaUVNoMm5NSnlSClJqNWdUeEVhMy9oa2g1QjVYV0ZPZW1PS3dMdW5Oc0NsMUFwYnppUTdqQmt2TGFXSERyQmFmNUFIbnhaajE4bHIKK0pkclNVdjBEZjV0UzhTNm50TmlnaVpNU1A0Z01reWt0QnloUjhVNk9UWUl0V0crT1FLcmxJdm14cGtFNk5qNgo0T2dSRnIwOGJVcXIvbE5qc0V6YWt0aGVCYzFmbDFzZjBlWFNCUlpyV08zY2Z3ZFBkd2EweU0zM051QUJnenh3CkpiMjNyZ0pSOWlISWp0YlBYaC80QWYySHdnWFNVTy9tSUhDawotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== 17 | rules: 18 | - operations: [ "CREATE" ] 19 | apiGroups: [""] 20 | apiVersions: ["v1"] 21 | resources: ["pods"] 22 | namespaceSelector: 23 | matchLabels: 24 | hmcts.github.com/envInjector: enabled 25 | -------------------------------------------------------------------------------- /deployment/mutatingwebhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: env-injector-webhook-cfg 5 | labels: 6 | app: env-injector 7 | webhooks: 8 | - name: env-injector.hmcts.net 9 | admissionReviewVersions: [v1beta1, v1] 10 | sideEffects: NoneOnDryRun 11 | clientConfig: 12 | service: 13 | name: env-injector-webhook-svc 14 | namespace: admin 15 | path: "/mutate" 16 | caBundle: ${CA_BUNDLE} 17 | rules: 18 | - operations: [ "CREATE" ] 19 | apiGroups: [""] 20 | apiVersions: ["v1"] 21 | resources: ["pods"] 22 | namespaceSelector: 23 | matchLabels: 24 | hmcts.github.com/envInjector: enabled 25 | -------------------------------------------------------------------------------- /deployment/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: env-injector-webhook-svc 5 | labels: 6 | app: env-injector 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 443 11 | selector: 12 | app: env-injector 13 | -------------------------------------------------------------------------------- /deployment/test_deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: sleep 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: sleep 10 | template: 11 | metadata: 12 | labels: 13 | app: sleep 14 | # annotations: 15 | # some-annotation-key-1: some_annotation_value_1 16 | spec: 17 | # dnsConfig: 18 | # options: 19 | # - name: use-vc 20 | # affinity: 21 | # podAntiAffinity: 22 | # requiredDuringSchedulingIgnoredDuringExecution: 23 | # - labelSelector: 24 | # matchExpressions: 25 | # - key: app 26 | # operator: In 27 | # values: 28 | # - sleep 29 | # topologyKey: "kubernetes.io/hostname" 30 | containers: 31 | - name: sleep 32 | image: tutum/curl 33 | command: ["/bin/sleep","infinity"] 34 | # env: 35 | # - name: SOME_KEY_1 36 | # value: SOME_VALUE_1 37 | -------------------------------------------------------------------------------- /deployment/webhook-create-signed-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | usage() { 6 | cat <> ${tmpdir}/csr.conf 63 | [req] 64 | req_extensions = v3_req 65 | distinguished_name = req_distinguished_name 66 | [req_distinguished_name] 67 | [ v3_req ] 68 | basicConstraints = CA:FALSE 69 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 70 | extendedKeyUsage = serverAuth 71 | subjectAltName = @alt_names 72 | [alt_names] 73 | DNS.1 = ${service} 74 | DNS.2 = ${service}.${namespace} 75 | DNS.3 = ${service}.${namespace}.svc 76 | EOF 77 | 78 | openssl genrsa -out ${tmpdir}/server-key.pem 2048 79 | openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=system:node:${service}.${namespace}.svc;/O=system:nodes" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf 80 | 81 | # clean-up any previously created CSR for our service. Ignore errors if not present. 82 | kubectl delete csr ${csrName} 2>/dev/null || true 83 | 84 | # create server cert/key CSR and send to k8s API 85 | cat <&2 121 | exit 1 122 | fi 123 | echo ${serverCert} | openssl base64 -d -A -out ${tmpdir}/server-cert.pem 124 | 125 | 126 | # create the secret with CA cert and server cert/key 127 | kubectl create secret generic ${secret} \ 128 | --from-file=key.pem=${tmpdir}/server-key.pem \ 129 | --from-file=cert.pem=${tmpdir}/server-cert.pem \ 130 | --dry-run=client -o yaml | 131 | kubectl -n ${namespace} apply -f - 132 | -------------------------------------------------------------------------------- /deployment/webhook-patch-ca-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ROOT=$(cd $(dirname $0)/../../; pwd) 4 | 5 | set -o errexit 6 | set -o nounset 7 | set -o pipefail 8 | 9 | export CA_BUNDLE=$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[].cluster.certificate-authority-data}') 10 | 11 | if command -v envsubst >/dev/null 2>&1; then 12 | envsubst 13 | else 14 | sed -e "s|\${CA_BUNDLE}|${CA_BUNDLE}|g" 15 | fi 16 | -------------------------------------------------------------------------------- /env-injector-azure-pipelines-image-publish.yaml: -------------------------------------------------------------------------------- 1 | name: k8s-env-injector_image_$(Build.BuildId)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:r) 2 | 3 | trigger: 4 | branches: 5 | include: 6 | - master 7 | paths: 8 | include: 9 | - image 10 | 11 | pr: none 12 | 13 | resources: 14 | repositories: 15 | - repository: cnp-azuredevops-libraries 16 | type: github 17 | name: hmcts/cnp-azuredevops-libraries 18 | endpoint: hmcts 19 | 20 | variables: 21 | - name: agentPool 22 | value: ubuntu-latest 23 | - name: acrName 24 | value: hmctspublic 25 | - name: acrResourceGroup 26 | value: rpe-acr-prod-rg 27 | - name: serviceConnection 28 | value: azurerm-prod 29 | - name: buildPath 30 | value: image 31 | - name: buildTime 32 | value: $[format('{0:yyyyMMdd}', pipeline.startTime)] 33 | 34 | jobs: 35 | - job: BuildAndPushImage 36 | pool: 37 | vmImage: ${{ variables.agentPool }} 38 | steps: 39 | - checkout: self 40 | clean: true 41 | 42 | - template: steps/acr-build.yaml@cnp-azuredevops-libraries 43 | parameters: 44 | serviceConnection: $(serviceConnection) 45 | customImageTag: $(Build.BuildId)_$(buildTime) 46 | buildPath: $(buildPath) -------------------------------------------------------------------------------- /env-injector-azure-pipelines.yaml: -------------------------------------------------------------------------------- 1 | name: k8s-env-injector_chart_$(Build.BuildId)_$(SourceBranchName)_$(Date:yyyyMMdd)$(Rev:r) 2 | 3 | trigger: 4 | branches: 5 | include: 6 | - refs/tags/* 7 | paths: 8 | exclude: 9 | - image 10 | 11 | pr: 12 | branches: 13 | include: 14 | - master 15 | 16 | resources: 17 | repositories: 18 | - repository: cnp-azuredevops-libraries 19 | type: github 20 | ref: refs/heads/master 21 | name: hmcts/cnp-azuredevops-libraries 22 | endpoint: 'hmcts' 23 | 24 | variables: 25 | - name: agentPool 26 | value: ubuntu-latest 27 | - name: acrName 28 | value: hmctspublic 29 | - name: acrResourceGroup 30 | value: rpe-acr-prod-rg 31 | - name: serviceConnection 32 | value: azurerm-prod 33 | - name: repoName 34 | value: k8s-env-injector 35 | - name: buildPath 36 | value: image 37 | - name: chartName 38 | value: env-injector-webhook 39 | - name: valuesFile 40 | value: $(chartName)/values.yaml 41 | - name: testAppImage 42 | value: hmctspublic.azurecr.io/$(Build.Repository.Name) 43 | - name: aksResourceGroup 44 | value: "cft-sbox-00-rg" 45 | - name: aksCluster 46 | value: "cft-sbox-00-aks" 47 | 48 | jobs: 49 | - job: Validate 50 | pool: 51 | vmImage: ${{ variables.agentPool }} 52 | steps: 53 | - checkout: self 54 | clean: true 55 | 56 | - ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: 57 | - template: steps/acr-build.yaml@cnp-azuredevops-libraries 58 | parameters: 59 | serviceConnection: $(serviceConnection) 60 | buildPath: $(repoName)/$(buildPath) 61 | 62 | - template: steps/charts/validate.yaml@cnp-azuredevops-libraries 63 | parameters: 64 | chartName: $(chartName) 65 | chartReleaseName: chart-$(chartName)-ci 66 | chartNamespace: chart-tests 67 | helmInstallTimeout: "300" 68 | valuesFile: $(valuesFile) 69 | serviceConnection: "DCD-CFTAPPS-SBOX" 70 | registryServiceConnection: "azurerm-prod" 71 | aksResourceGroup: $(aksResourceGroup) 72 | aksCluster: $(aksCluster) 73 | ${{ if eq(variables['Build.Reason'], 'PullRequest') }}: 74 | additionalHelmArgs: --set image=$(testAppImage):pr-$(System.PullRequest.PullRequestNumber) 75 | 76 | - job: Release 77 | # Make sure we have a tag to run this job 78 | condition: > 79 | and( 80 | succeeded(), 81 | startsWith(variables['Build.SourceBranch'], 'refs/tags/') 82 | ) 83 | dependsOn: Validate 84 | pool: 85 | vmImage: 'ubuntu-latest' 86 | steps: 87 | - template: steps/charts/release.yaml@cnp-azuredevops-libraries 88 | parameters: 89 | chartName: $(chartName) 90 | chartReleaseName: chart-$(chartName)-ci 91 | chartNamespace: chart-tests 92 | serviceConnection: "DCD-CFTAPPS-SBOX" 93 | registryServiceConnection: "azurerm-prod" 94 | aksResourceGroup: $(aksResourceGroup) 95 | aksCluster: $(aksCluster) 96 | 97 | -------------------------------------------------------------------------------- /env-injector-webhook/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /env-injector-webhook/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for kubernetes environment injector mutating webhook 4 | name: env-injector-webhook 5 | home: https://github.com/hmcts/k8s-env-injector 6 | version: 0.1.1 7 | maintainers: 8 | - name: HMCTS Platform engineering team 9 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "chart-env-injector.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "chart-env-injector.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "chart-env-injector.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "chart-env-injector.labels" }} 38 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 39 | helm.sh/chart: {{ include "chart-env-injector.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | 47 | {{/* 48 | Template to add the environment variable list and checking the format of the keys 49 | The key or "environment variable" must be uppercase and contain only numbers or "_". 50 | */}} 51 | {{- define "chart-env-injector.environment" -}} 52 | {{- if .Values.environment -}} 53 | {{- range $key, $val := .Values.environment }} 54 | - name: {{ if $key | regexMatch "^[^.-]+$" -}} 55 | {{- $key }} 56 | {{- else -}} 57 | {{- fail (join "Environment variables can not contain '.' or '-' Failed key: " ($key|quote)) -}} 58 | {{- end }} 59 | value: {{ tpl ($val | quote) $ }} 60 | {{- end }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Template to add the dns options 66 | */}} 67 | {{- define "chart-env-injector.dnsOptions" -}} 68 | {{- if .Values.dnsOptions -}} 69 | {{- range $key, $val := .Values.dnsOptions }} 70 | - name: {{ $key }} 71 | {{- if $val }} 72 | value: {{ tpl ($val | quote) $ }} 73 | {{- end }} 74 | {{- end }} 75 | {{- end }} 76 | {{- end }} 77 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-deployment 5 | labels: 6 | {{- ( include "chart-env-injector.labels" . ) | indent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicas }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 12 | template: 13 | metadata: 14 | labels: 15 | {{- ( include "chart-env-injector.labels" . ) | indent 8 }} 16 | spec: 17 | containers: 18 | - name: env-injector 19 | image: {{ .Values.image }} 20 | imagePullPolicy: IfNotPresent 21 | args: 22 | - -envCfgFile=/etc/webhook/config/envconfig.yaml 23 | - -tlsCertFile=/etc/webhook/certs/cert.pem 24 | - -tlsKeyFile=/etc/webhook/certs/key.pem 25 | - -alsologtostderr 26 | - -v=4 27 | - 2>&1 28 | volumeMounts: 29 | - name: webhook-certs 30 | mountPath: /etc/webhook/certs 31 | readOnly: true 32 | - name: webhook-config 33 | mountPath: /etc/webhook/config 34 | volumes: 35 | - name: webhook-certs 36 | secret: 37 | secretName: {{ include "chart-env-injector.name" . }}-certs 38 | - name: webhook-config 39 | configMap: 40 | name: {{ include "chart-env-injector.name" . }}-configmap 41 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/env-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-configmap 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 8 | app.kubernetes.io/managed-by: {{ .Release.Service }} 9 | helm.sh/chart: {{ template "chart-env-injector.chart" . }} 10 | release: {{ .Release.Name }} 11 | data: 12 | envconfig.yaml: | 13 | env: 14 | {{- (include "chart-env-injector.environment" .) | indent 6 }} 15 | dnsOptions: 16 | {{- (include "chart-env-injector.dnsOptions" .) | indent 6 }} 17 | {{- if .Values.removePodAntiAffinity }} 18 | removePodAntiAffinity: {{ .Values.removePodAntiAffinity }} 19 | {{- end }} 20 | {{- if .Values.requiredNodeAffinityTerms }} 21 | requiredNodeAffinityTerms: 22 | {{ tpl (toYaml .Values.requiredNodeAffinityTerms | indent 6) . }} 23 | {{- end }} 24 | {{- if .Values.preferredNodeAffinityTerms }} 25 | preferredNodeAffinityTerms: 26 | {{ tpl (toYaml .Values.preferredNodeAffinityTerms | indent 6) . }} 27 | {{- end }} 28 | {{- if .Values.tolerations }} 29 | tolerations: 30 | {{ tpl (toYaml .Values.tolerations | indent 6) . }} 31 | {{- end }} 32 | {{- if .Values.topologyConstraints }} 33 | topologyConstraints: 34 | {{ tpl (toYaml .Values.topologyConstraints | indent 6) . }} 35 | {{- end }} -------------------------------------------------------------------------------- /env-injector-webhook/templates/mutatingwebhook.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: admissionregistration.k8s.io/v1 2 | kind: MutatingWebhookConfiguration 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-cfg 5 | labels: 6 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 7 | app.kubernetes.io/managed-by: {{ .Release.Service }} 8 | helm.sh/chart: {{ template "chart-env-injector.chart" . }} 9 | release: {{ .Release.Name }} 10 | annotations: 11 | "helm.sh/hook": pre-install,pre-upgrade 12 | "helm.sh/hook-delete-policy": before-hook-creation 13 | "helm.sh/hook-weight": "-5" 14 | webhooks: 15 | - name: env-injector.hmcts.net 16 | admissionReviewVersions: [v1beta1, v1] 17 | sideEffects: NoneOnDryRun 18 | clientConfig: 19 | service: 20 | name: {{ include "chart-env-injector.name" . }}-svc 21 | namespace: {{ .Release.Namespace }} 22 | path: "/mutate" 23 | rules: 24 | - operations: [ "CREATE" ] 25 | apiGroups: [""] 26 | apiVersions: ["v1"] 27 | resources: ["pods"] 28 | namespaceSelector: 29 | matchLabels: 30 | hmcts.github.com/envInjector: enabled 31 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/post-delete-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-cleanup-config 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 8 | app.kubernetes.io/managed-by: {{ .Release.Service }} 9 | helm.sh/chart: {{ template "chart-env-injector.chart" . }} 10 | release: {{ .Release.Name }} 11 | annotations: 12 | "helm.sh/hook": post-delete,pre-upgrade 13 | "helm.sh/hook-delete-policy": before-hook-creation 14 | "helm.sh/hook-weight": "-8" 15 | data: 16 | config.sh: | 17 | #!/bin/bash 18 | 19 | kubectl delete mutatingwebhookconfigurations.admissionregistration.k8s.io {{ include "chart-env-injector.name" . }}-cfg 20 | kubectl delete secret {{ include "chart-env-injector.name" . }}-certs -n {{ .Release.Namespace }} 21 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/post-delete-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-cleanup-job 5 | labels: 6 | app.kubernetes.io/managed-by: {{ .Release.Service }} 7 | app.kubernetes.io/instance: {{ .Release.Name }} 8 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 9 | app.kubernetes.io/name: {{ template "chart-env-injector.name" . }} 10 | annotations: 11 | "helm.sh/hook": post-delete,pre-upgrade 12 | "helm.sh/hook-delete-policy": before-hook-creation 13 | "helm.sh/hook-weight": "-6" 14 | spec: 15 | template: 16 | metadata: 17 | name: {{ include "chart-env-injector.name" . }}-cleanup-job-tpl 18 | labels: 19 | app.kubernetes.io/instance: {{ .Release.Name }} 20 | app.kubernetes.io/name: {{ template "chart-env-injector.name" . }} 21 | spec: 22 | restartPolicy: OnFailure 23 | serviceAccountName: env-injector 24 | containers: 25 | - name: post-delete-job 26 | image: bitnami/kubectl:latest 27 | command: ["/bin/sh", "-c", "/etc/config/config.sh"] 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/config 31 | volumes: 32 | - name: config-volume 33 | configMap: 34 | name: {{ include "chart-env-injector.name" . }}-cleanup-config 35 | defaultMode: 0755 36 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/pre-install-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-ca-config 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 8 | app.kubernetes.io/managed-by: {{ .Release.Service }} 9 | helm.sh/chart: {{ template "chart-env-injector.chart" . }} 10 | release: {{ .Release.Name }} 11 | annotations: 12 | "helm.sh/hook": pre-install,pre-upgrade 13 | "helm.sh/hook-delete-policy": before-hook-creation 14 | "helm.sh/hook-weight": "-3" 15 | data: 16 | config.sh: | 17 | #!/bin/bash 18 | 19 | set -e 20 | 21 | usage() { 22 | cat <> ${tmpdir}/csr.conf 85 | [req] 86 | req_extensions = v3_req 87 | distinguished_name = req_distinguished_name 88 | [req_distinguished_name] 89 | [ v3_req ] 90 | basicConstraints = CA:FALSE 91 | keyUsage = nonRepudiation, digitalSignature, keyEncipherment 92 | extendedKeyUsage = serverAuth 93 | subjectAltName = @alt_names 94 | [alt_names] 95 | DNS.1 = ${service} 96 | DNS.2 = ${service}.${namespace} 97 | DNS.3 = ${service}.${namespace}.svc 98 | EOF 99 | 100 | openssl genrsa -out ${tmpdir}/server-key.pem 2048 101 | openssl req -new -key ${tmpdir}/server-key.pem -subj "/CN=system:node:${service}.${namespace}.svc;/O=system:nodes" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf 102 | 103 | # clean-up any previously created CSR for our service. Ignore errors if not present. 104 | kubectl delete csr ${csrName} 2>/dev/null || true 105 | 106 | # create server cert/key CSR and send to k8s API 107 | cat <&2 143 | exit 1 144 | fi 145 | echo ${serverCert} | openssl base64 -d -A -out ${tmpdir}/server-cert.pem 146 | 147 | 148 | # create the secret with CA cert and server cert/key 149 | kubectl create secret generic ${secret} \ 150 | --from-file=key.pem=${tmpdir}/server-key.pem \ 151 | --from-file=cert.pem=${tmpdir}/server-cert.pem \ 152 | --dry-run=client -o yaml | 153 | kubectl -n ${namespace} apply -f - 154 | 155 | 156 | # Patch the webhook adding the caBundle. 157 | set +e 158 | export caBundle=$(kubectl get configmap -n kube-system extension-apiserver-authentication -o=jsonpath='{.data.client-ca-file}' | base64 | tr -d '\n') 159 | [ -z "${caBundle}" ] && echo "ERROR: cannot get CA Bundle" && exit 1 160 | while true; do 161 | echo "INFO: Trying to patch webhook adding the caBundle." 162 | if kubectl patch mutatingwebhookconfiguration "${webhook}" --type='json' -p "[{'op': 'add', 'path': '/webhooks/0/clientConfig/caBundle', 'value':'${caBundle}'}]"; then 163 | break 164 | fi 165 | echo "INFO: webhook not patched. Retrying in 5s..." 166 | sleep 5 167 | done 168 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/pre-install-job.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-ca-config-job 5 | labels: 6 | app.kubernetes.io/managed-by: {{ .Release.Service }} 7 | app.kubernetes.io/instance: {{ .Release.Name }} 8 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 9 | app.kubernetes.io/name: {{ template "chart-env-injector.name" . }} 10 | annotations: 11 | "helm.sh/hook": pre-install,pre-upgrade 12 | "helm.sh/hook-delete-policy": before-hook-creation 13 | "helm.sh/hook-weight": "-2" 14 | spec: 15 | template: 16 | metadata: 17 | name: {{ include "chart-env-injector.name" . }}-ca-config-job-tpl 18 | labels: 19 | app.kubernetes.io/instance: {{ .Release.Name }} 20 | app.kubernetes.io/name: {{ template "chart-env-injector.name" . }} 21 | spec: 22 | restartPolicy: OnFailure 23 | serviceAccountName: env-injector 24 | containers: 25 | - name: pre-install-job 26 | image: bitnami/kubectl:latest 27 | command: ["/bin/sh", "-c", "/etc/config/config.sh"] 28 | volumeMounts: 29 | - name: config-volume 30 | mountPath: /etc/config 31 | volumes: 32 | - name: config-volume 33 | configMap: 34 | name: {{ include "chart-env-injector.name" . }}-ca-config 35 | defaultMode: 0755 36 | -------------------------------------------------------------------------------- /env-injector-webhook/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: env-injector 5 | annotations: 6 | "helm.sh/hook": pre-install,pre-upgrade 7 | "helm.sh/hook-delete-policy": before-hook-creation 8 | "helm.sh/hook-weight": "-3" 9 | rules: 10 | - apiGroups: 11 | - '' 12 | resources: 13 | - 'pods' 14 | - 'namespaces' 15 | - 'configmaps' 16 | verbs: 17 | - 'get' 18 | - 'list' 19 | - 'patch' 20 | - apiGroups: 21 | - 'certificates.k8s.io' 22 | resources: 23 | - '*' 24 | verbs: 25 | - '*' 26 | - apiGroups: 27 | - '' 28 | resources: 29 | - 'secrets' 30 | verbs: 31 | - '*' 32 | - apiGroups: 33 | - 'admissionregistration.k8s.io' 34 | resources: 35 | - 'mutatingwebhookconfigurations' 36 | verbs: 37 | - '*' 38 | --- 39 | 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: ClusterRoleBinding 42 | metadata: 43 | name: env-injector 44 | annotations: 45 | "helm.sh/hook": pre-install,pre-upgrade 46 | "helm.sh/hook-delete-policy": before-hook-creation 47 | "helm.sh/hook-weight": "-3" 48 | roleRef: 49 | apiGroup: rbac.authorization.k8s.io 50 | kind: ClusterRole 51 | name: env-injector 52 | subjects: 53 | - kind: ServiceAccount 54 | name: env-injector 55 | namespace: {{ .Release.Namespace }} 56 | 57 | --- 58 | 59 | apiVersion: v1 60 | kind: ServiceAccount 61 | metadata: 62 | name: env-injector 63 | annotations: 64 | "helm.sh/hook": pre-install,pre-upgrade 65 | "helm.sh/hook-delete-policy": before-hook-creation 66 | "helm.sh/hook-weight": "-3" 67 | namespace: {{ .Release.Namespace }} -------------------------------------------------------------------------------- /env-injector-webhook/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "chart-env-injector.name" . }}-svc 5 | labels: 6 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 7 | app.kubernetes.io/managed-by: {{ .Release.Service }} 8 | helm.sh/chart: {{ template "chart-env-injector.chart" . }} 9 | release: {{ .Release.Name }} 10 | spec: 11 | ports: 12 | - port: 443 13 | targetPort: 443 14 | selector: 15 | app.kubernetes.io/name: {{ include "chart-env-injector.name" . }} 16 | -------------------------------------------------------------------------------- /env-injector-webhook/test-pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: sleep 5 | namespace: rpe 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: sleep 11 | template: 12 | metadata: 13 | labels: 14 | app: sleep 15 | spec: 16 | containers: 17 | - name: sleep 18 | image: hmctspublic.azurecr.io/docker-curl 19 | command: ["sleep","1d"] 20 | imagePullPolicy: Always 21 | -------------------------------------------------------------------------------- /env-injector-webhook/values.yaml: -------------------------------------------------------------------------------- 1 | image: hmctspublic.azurecr.io/hmcts/k8s-env-injector:496359_20231218 2 | replicas: 2 3 | removePodAntiAffinity: false 4 | environment: {} 5 | # CLUSTER_NAME: aks-test-01 6 | dnsOptions: {} 7 | # ndots: 3 8 | # single-request-reopen: 9 | # use-vc: 10 | requiredNodeAffinityTerms: {} 11 | # - matchExpressions: 12 | # - key: agentpool 13 | # operator: In 14 | # values: 15 | # - ubuntu18 16 | # - ubuntu1804 17 | preferredNodeAffinityTerms: {} 18 | # - weight: 1 19 | # preference: 20 | # matchExpressions: 21 | # - key: kubernetes.azure.com/scalesetpriority 22 | # operator: DoesNotExist 23 | tolerations: {} 24 | # - key: kubernetes.azure.com/scalesetpriority 25 | # effect: NoSchedule 26 | # operator: Equal 27 | # value: spot 28 | topologyConstraints: {} 29 | # - maxSkew: 1 30 | # topologyKey: topology.kubernetes.io/zone 31 | # whenUnsatisfiable: ScheduleAnyway 32 | # nodeAffinityPolicy: Honor 33 | # nodeTaintsPolicy: Honor 34 | # labelSelector: 35 | # matchLabels: 36 | # app.kubernetes.io/name: test-app 37 | # matchLabelKeys: 38 | # - pod-template-hash 39 | -------------------------------------------------------------------------------- /image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.3-bullseye as build 2 | 3 | WORKDIR /go/src/app 4 | ADD . /go/src/app 5 | RUN go get -u -t ./... 6 | RUN CGO_ENABLED=0 GOOS=linux GO111MODULE="on" go build -a -installsuffix cgo -o /go/bin/app/k8s-env-injector . 7 | 8 | 9 | FROM gcr.io/distroless/static 10 | 11 | COPY --from=build /go/bin/app / 12 | ENTRYPOINT ["./k8s-env-injector"] 13 | -------------------------------------------------------------------------------- /image/add_affinities.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | // addRequiredNodeAffinityTerms performs the mutation(s) needed to add selector terms to the node affinity 9 | // RequiredDuringSchedulingIgnoredDuringExecution section of to the target resource 10 | func addRequiredNodeAffinityTerms(target, requiredNodeAffinityTerms []corev1.NodeSelectorTerm, basePath string) (patch []patchOperation) { 11 | first := len(target) == 0 12 | var value interface{} 13 | for i, rna := range requiredNodeAffinityTerms { 14 | value = rna 15 | path := basePath 16 | var skip bool 17 | var op string 18 | if first { 19 | first = false 20 | op = "add" 21 | value = []corev1.NodeSelectorTerm{rna} 22 | } else { 23 | optExists := false 24 | for idx, targetOpt := range target { 25 | if len(targetOpt.MatchExpressions) > 0 { 26 | matchExpr := targetOpt.MatchExpressions[i] 27 | rnaMatchExpr := rna.MatchExpressions[i] 28 | keyEqual := cmp.Equal(matchExpr.Key, rnaMatchExpr.Key) 29 | if keyEqual { 30 | optExists = true 31 | operatorEqual := cmp.Equal(matchExpr.Operator, rnaMatchExpr.Operator) 32 | valuesEqual := cmp.Equal(matchExpr.Values, rnaMatchExpr.Values) 33 | 34 | skip, op, path = checkReplaceOrSkip(idx, path, operatorEqual, valuesEqual) 35 | 36 | } 37 | } 38 | 39 | if len(targetOpt.MatchFields) > 0 { 40 | matchFlds := targetOpt.MatchFields[i] 41 | rnamatchFlds := rna.MatchFields[i] 42 | keyEqual := cmp.Equal(matchFlds.Key, rnamatchFlds.Key) 43 | if keyEqual { 44 | optExists = true 45 | operatorEqual := cmp.Equal(matchFlds.Operator, rnamatchFlds.Operator) 46 | valuesEqual := cmp.Equal(matchFlds.Values, rnamatchFlds.Values) 47 | 48 | skip, op, path = checkReplaceOrSkip(idx, path, operatorEqual, valuesEqual) 49 | } 50 | } 51 | } 52 | if !optExists { 53 | op = "add" 54 | path = path + "/-" 55 | } 56 | } 57 | if !skip { 58 | patch = append(patch, patchOperation{ 59 | Op: op, 60 | Path: path, 61 | Value: value, 62 | }) 63 | } else { 64 | patch = []patchOperation{} 65 | } 66 | } 67 | return patch 68 | } 69 | 70 | // addPreferredNodeAffinityTerms performs the mutation(s) needed to add selector terms to the node affinity 71 | // preferredDuringSchedulingIgnoredDuringExecution section of to the target resource 72 | func addPreferredNodeAffinityTerms(target, preferredNodeAffinityTerms []corev1.PreferredSchedulingTerm, basePath string) (patch []patchOperation) { 73 | first := len(target) == 0 74 | var value interface{} 75 | for i, pna := range preferredNodeAffinityTerms { 76 | value = pna 77 | path := basePath 78 | skip := false 79 | var op string 80 | if first { 81 | first = false 82 | op = "add" 83 | value = []corev1.PreferredSchedulingTerm{pna} 84 | } else { 85 | optExists := false 86 | for idx, targetOpt := range target { 87 | if len(targetOpt.Preference.MatchExpressions) > 0 { 88 | matchExpr := targetOpt.Preference.MatchExpressions[i] 89 | pnaMatchExpr := pna.Preference.MatchExpressions[i] 90 | keyEqual := cmp.Equal(matchExpr.Key, pnaMatchExpr.Key) 91 | if keyEqual { 92 | optExists = true 93 | operatorEqual := cmp.Equal(matchExpr.Operator, pnaMatchExpr.Operator) 94 | valuesEqual := cmp.Equal(matchExpr.Values, pnaMatchExpr.Values) 95 | weightEqual := cmp.Equal(targetOpt.Weight, pna.Weight) 96 | 97 | skip, op, path = checkReplaceOrSkip(idx, path, operatorEqual, valuesEqual, weightEqual) 98 | 99 | } 100 | } 101 | 102 | if len(targetOpt.Preference.MatchFields) > 0 { 103 | 104 | matchExpr := targetOpt.Preference.MatchFields[i] 105 | pnaMatchExpr := pna.Preference.MatchFields[i] 106 | keyEqual := cmp.Equal(matchExpr.Key, pnaMatchExpr.Key) 107 | if keyEqual { 108 | optExists = true 109 | operatorEqual := cmp.Equal(matchExpr.Operator, pnaMatchExpr.Operator) 110 | valuesEqual := cmp.Equal(matchExpr.Values, pnaMatchExpr.Values) 111 | weightEqual := cmp.Equal(targetOpt.Weight, pna.Weight) 112 | 113 | skip, op, path = checkReplaceOrSkip(idx, path, operatorEqual, valuesEqual, weightEqual) 114 | } 115 | } 116 | } 117 | if !optExists { 118 | op = "add" 119 | path = path + "/-" 120 | } 121 | } 122 | if !skip { 123 | patch = append(patch, patchOperation{ 124 | Op: op, 125 | Path: path, 126 | Value: value, 127 | }) 128 | } else { 129 | patch = []patchOperation{} 130 | } 131 | } 132 | return patch 133 | } 134 | -------------------------------------------------------------------------------- /image/add_dns_options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | // addDnsOptions performs the mutation(s) needed to add the extra dnsOptions to the target 9 | // resource 10 | func addDnsOptions(target, dnsOptions []corev1.PodDNSConfigOption, basePath string) (patch []patchOperation) { 11 | first := len(target) == 0 12 | var value interface{} 13 | for _, dnsOpt := range dnsOptions { 14 | value = dnsOpt 15 | path := basePath 16 | var skip bool 17 | var op string 18 | if first { 19 | first = false 20 | op = "add" 21 | value = []corev1.PodDNSConfigOption{dnsOpt} 22 | } else { 23 | optExists := false 24 | for idx, targetOpt := range target { 25 | nameEqual := cmp.Equal(targetOpt.Name, dnsOpt.Name) 26 | if nameEqual { 27 | optExists = true 28 | valueEqual := cmp.Equal(targetOpt.Value, dnsOpt.Value) 29 | 30 | skip, op, path = checkReplaceOrSkip(idx, path, valueEqual) 31 | } 32 | } 33 | if !optExists { 34 | op = "add" 35 | path = path + "/-" 36 | } 37 | } 38 | if !skip { 39 | patch = append(patch, patchOperation{ 40 | Op: op, 41 | Path: path, 42 | Value: value, 43 | }) 44 | } else { 45 | patch = []patchOperation{} 46 | } 47 | } 48 | return patch 49 | } 50 | -------------------------------------------------------------------------------- /image/add_env_var.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | // addEnv performs the mutation(s) needed to add the extra environment variables to the target 9 | // resource 10 | func addEnv(target, envVars []corev1.EnvVar, basePath string) (patch []patchOperation) { 11 | first := len(target) == 0 12 | var value interface{} 13 | for _, envVar := range envVars { 14 | value = envVar 15 | path := basePath 16 | var skip bool 17 | var op string 18 | if first { 19 | first = false 20 | op = "add" 21 | value = []corev1.EnvVar{envVar} 22 | } else { 23 | 24 | optExists := false 25 | for idx, targetOpt := range target { 26 | nameEqual := cmp.Equal(targetOpt.Name, envVar.Name) 27 | if nameEqual { 28 | optExists = true 29 | valueEqual := cmp.Equal(targetOpt.Value, envVar.Value) 30 | valueFromEqual := cmp.Equal(targetOpt.ValueFrom, envVar.ValueFrom) 31 | 32 | skip, op, path = checkReplaceOrSkip(idx, path, valueEqual, valueFromEqual) 33 | } 34 | } 35 | if !optExists { 36 | op = "add" 37 | path = path + "/-" 38 | } 39 | } 40 | if !skip { 41 | patch = append(patch, patchOperation{ 42 | Op: op, 43 | Path: path, 44 | Value: value, 45 | }) 46 | } else { 47 | patch = []patchOperation{} 48 | } 49 | } 50 | return patch 51 | } 52 | -------------------------------------------------------------------------------- /image/add_tolerations.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | // addToleration performs the mutation(s) needed to add the extra tolerations to the target resource 9 | func addTolerations(target, Tolerations []corev1.Toleration, basePath string) (patch []patchOperation) { 10 | first := len(target) == 0 11 | var value interface{} 12 | for _, tol := range Tolerations { 13 | value = tol 14 | path := basePath 15 | var skip bool 16 | var op string 17 | if first { 18 | first = false 19 | op = "add" 20 | value = []corev1.Toleration{tol} 21 | } else { 22 | optExists := false 23 | for idx, targetOpt := range target { 24 | keyEqual := cmp.Equal(targetOpt.Key, tol.Key) 25 | if keyEqual { 26 | optExists = true 27 | operatorEqual := cmp.Equal(targetOpt.Operator, tol.Operator) 28 | effectEqual := cmp.Equal(targetOpt.Effect, tol.Effect) 29 | valueEqual := cmp.Equal(targetOpt.Value, tol.Value) 30 | 31 | skip, op, path = checkReplaceOrSkip(idx, path, operatorEqual, effectEqual, valueEqual) 32 | } 33 | } 34 | if !optExists { 35 | op = "add" 36 | path = path + "/-" 37 | } 38 | } 39 | if !skip { 40 | patch = append(patch, patchOperation{ 41 | Op: op, 42 | Path: path, 43 | Value: value, 44 | }) 45 | } else { 46 | patch = []patchOperation{} 47 | } 48 | } 49 | return patch 50 | } 51 | -------------------------------------------------------------------------------- /image/add_topology_constraints.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | // addTopologySpreadConstraints performs the mutation(s) needed to add Topology Spread Constraints to your resource 9 | func addTopologySpreadConstraints(target, TopologyConstraints []corev1.TopologySpreadConstraint, basePath string) (patch []patchOperation) { 10 | first := len(target) == 0 11 | var value interface{} 12 | for _, tsc := range TopologyConstraints { 13 | value = tsc 14 | path := basePath 15 | var skip bool 16 | var op string 17 | if first { 18 | first = false 19 | op = "add" 20 | value = []corev1.TopologySpreadConstraint{tsc} 21 | } else { 22 | optExists := false 23 | for idx, targetOpt := range target { 24 | 25 | keyEqual := cmp.Equal(targetOpt.TopologyKey, tsc.TopologyKey) 26 | if keyEqual { 27 | optExists = true 28 | skewEqual := cmp.Equal(targetOpt.MaxSkew, tsc.MaxSkew) 29 | nodeAffinityEqual := cmp.Equal(targetOpt.NodeAffinityPolicy, tsc.NodeAffinityPolicy) 30 | nodeTaintEqual := cmp.Equal(targetOpt.NodeTaintsPolicy, tsc.NodeTaintsPolicy) 31 | unsatisfiableEqual := cmp.Equal(targetOpt.WhenUnsatisfiable, tsc.WhenUnsatisfiable) 32 | labelSelectorEqual := cmp.Equal(targetOpt.LabelSelector, tsc.LabelSelector) 33 | matchLabelKeysEqual := cmp.Equal(targetOpt.MatchLabelKeys, tsc.MatchLabelKeys) 34 | 35 | skip, op, path = checkReplaceOrSkip(idx, path, skewEqual, nodeAffinityEqual, nodeTaintEqual, unsatisfiableEqual, labelSelectorEqual, matchLabelKeysEqual) 36 | } 37 | } 38 | if !optExists { 39 | op = "add" 40 | path = path + "/-" 41 | } 42 | } 43 | if !skip { 44 | patch = append(patch, patchOperation{ 45 | Op: op, 46 | Path: path, 47 | Value: value, 48 | }) 49 | } else { 50 | patch = []patchOperation{} 51 | } 52 | } 53 | return patch 54 | } 55 | -------------------------------------------------------------------------------- /image/create_patch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | // createPatch creates a mutation patch for resources 11 | func createPatch(pod *corev1.Pod, envConfig *Config, annotations map[string]string) ([]byte, error) { 12 | var patches []patchOperation 13 | 14 | for idx, container := range pod.Spec.Containers { 15 | patches = append(patches, addEnv(container.Env, envConfig.Env, fmt.Sprintf("/spec/containers/%d/env", idx))...) 16 | } 17 | if len(envConfig.DnsOptions) > 0 { 18 | if pod.Spec.DNSConfig == nil { 19 | pod.Spec.DNSConfig = &corev1.PodDNSConfig{} 20 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/dnsConfig", Value: corev1.PodDNSConfig{}}) 21 | } 22 | patches = append(patches, addDnsOptions(pod.Spec.DNSConfig.Options, envConfig.DnsOptions, fmt.Sprintf("/spec/dnsConfig/options"))...) 23 | } 24 | if len(envConfig.Tolerations) > 0 { 25 | if pod.Spec.Tolerations == nil { 26 | pod.Spec.Tolerations = []corev1.Toleration{} 27 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/tolerations", Value: []corev1.Toleration{}}) 28 | } 29 | patches = append(patches, addTolerations(pod.Spec.Tolerations, envConfig.Tolerations, fmt.Sprintf("/spec/tolerations"))...) 30 | } 31 | if len(envConfig.TopologyConstraints) > 0 { 32 | if pod.Spec.TopologySpreadConstraints == nil { 33 | pod.Spec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{} 34 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/topologySpreadConstraints", Value: []corev1.TopologySpreadConstraint{}}) 35 | } 36 | patches = append(patches, addTopologySpreadConstraints(pod.Spec.TopologySpreadConstraints, envConfig.TopologyConstraints, fmt.Sprintf("/spec/topologySpreadConstraints"))...) 37 | } 38 | if envConfig.RemovePodAntiAffinity { 39 | if pod.Spec.Affinity != nil && pod.Spec.Affinity.PodAntiAffinity != nil { 40 | // Remove PodAntiAffinity 41 | patches = append(patches, removePodAntiAffinity("/spec/affinity/podAntiAffinity")...) 42 | } 43 | } 44 | if len(envConfig.RequiredNodeAffinityTerms) > 0 { 45 | if pod.Spec.Affinity == nil { 46 | pod.Spec.Affinity = &corev1.Affinity{} 47 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/affinity", Value: corev1.Affinity{}}) 48 | } 49 | if pod.Spec.Affinity.NodeAffinity == nil { 50 | pod.Spec.Affinity.NodeAffinity = &corev1.NodeAffinity{} 51 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/affinity/nodeAffinity", Value: corev1.NodeAffinity{}}) 52 | } 53 | if pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil { 54 | pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = &corev1.NodeSelector{} 55 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", Value: corev1.NodeSelector{}}) 56 | } 57 | patches = append(patches, addRequiredNodeAffinityTerms(pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, 58 | envConfig.RequiredNodeAffinityTerms, fmt.Sprintf("/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/nodeSelectorTerms"))...) 59 | } 60 | if len(envConfig.PreferredNodeAffinityTerms) > 0 { 61 | if pod.Spec.Affinity == nil { 62 | pod.Spec.Affinity = &corev1.Affinity{} 63 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/affinity", Value: corev1.Affinity{}}) 64 | } 65 | if pod.Spec.Affinity.NodeAffinity == nil { 66 | pod.Spec.Affinity.NodeAffinity = &corev1.NodeAffinity{} 67 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/affinity/nodeAffinity", Value: corev1.NodeAffinity{}}) 68 | } 69 | if pod.Spec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution == nil { 70 | pod.Spec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution = []corev1.PreferredSchedulingTerm{} 71 | patches = append(patches, patchOperation{Op: "add", Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", Value: []corev1.PreferredSchedulingTerm{}}) 72 | } 73 | patches = append(patches, addPreferredNodeAffinityTerms(pod.Spec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution, 74 | envConfig.PreferredNodeAffinityTerms, "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution")...) 75 | } 76 | 77 | patches = append(patches, updateAnnotation(pod.Annotations, annotations)...) 78 | 79 | return json.Marshal(patches) 80 | } 81 | -------------------------------------------------------------------------------- /image/go.mod: -------------------------------------------------------------------------------- 1 | module k8s-env-injector 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.3 6 | 7 | require ( 8 | github.com/ghodss/yaml v1.0.0 9 | github.com/golang/glog v1.2.1 10 | github.com/google/go-cmp v0.6.0 11 | k8s.io/api v0.30.1 12 | k8s.io/apimachinery v0.30.1 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/go-logr/logr v1.4.1 // indirect 18 | github.com/gogo/protobuf v1.3.2 // indirect 19 | github.com/google/gofuzz v1.2.0 // indirect 20 | github.com/json-iterator/go v1.1.12 // indirect 21 | github.com/kr/pretty v0.3.1 // indirect 22 | github.com/kr/text v0.2.0 // indirect 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 24 | github.com/modern-go/reflect2 v1.0.2 // indirect 25 | github.com/pmezard/go-difflib v1.0.0 // indirect 26 | github.com/rogpeppe/go-internal v1.11.0 // indirect 27 | github.com/spf13/pflag v1.0.5 // indirect 28 | github.com/stretchr/testify v1.8.4 // indirect 29 | golang.org/x/net v0.23.0 // indirect 30 | golang.org/x/text v0.14.0 // indirect 31 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 32 | gopkg.in/inf.v0 v0.9.1 // indirect 33 | gopkg.in/yaml.v2 v2.4.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | k8s.io/klog/v2 v2.120.1 // indirect 36 | k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect 37 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 38 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 39 | sigs.k8s.io/yaml v1.4.0 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /image/go.sum: -------------------------------------------------------------------------------- 1 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 2 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 3 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 4 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 5 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 6 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 15 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 16 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 17 | github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 18 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 19 | github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 20 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 21 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 22 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 23 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 24 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 25 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 26 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= 27 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 28 | github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 29 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 30 | github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= 31 | github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 32 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 33 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 34 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 35 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 36 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 37 | github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 38 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 39 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 40 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 41 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 42 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 43 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 44 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= 45 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 46 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 47 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 48 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 49 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 50 | github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= 51 | github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 52 | github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= 53 | github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 54 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 55 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 56 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 57 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 58 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 59 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 60 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 61 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 62 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 63 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 64 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 65 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 66 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 67 | github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= 68 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 69 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 70 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 71 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 72 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 73 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 74 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 75 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 76 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 77 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 78 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 79 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 80 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 81 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 82 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 83 | github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 84 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 85 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 86 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 87 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 88 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 89 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 90 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 91 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 92 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 93 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 94 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 95 | github.com/json-iterator/go v1.1.8 h1:QiWkFLKq0T7mpzwOTu6BzNDbfTE8OLrYhVKYMLF46Ok= 96 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 97 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 98 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 99 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 100 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 101 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 102 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 103 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 104 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 105 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 106 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 107 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 108 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 109 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 110 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 111 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 112 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 113 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 114 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 115 | github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= 116 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 117 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 118 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 119 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 120 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 121 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 122 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 123 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 124 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 125 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 126 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 127 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 128 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 129 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 130 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 131 | github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 132 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 133 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 134 | github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= 135 | github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= 136 | github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= 137 | github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= 138 | github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= 139 | github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= 140 | github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= 141 | github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= 142 | github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= 143 | github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= 144 | github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= 145 | github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= 146 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 147 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 148 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 149 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 150 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 151 | github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= 152 | github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 153 | github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= 154 | github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= 155 | github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= 156 | github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= 157 | github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= 158 | github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= 159 | github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= 160 | github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= 161 | github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= 162 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 163 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 164 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 165 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 166 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 167 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 168 | github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 169 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 170 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 171 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 172 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 173 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 174 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 175 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 176 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 177 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 178 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 179 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 180 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 181 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 182 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 183 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 184 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 185 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 186 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 187 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 188 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 189 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 190 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 191 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 192 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 193 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 194 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 195 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 196 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 197 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 198 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 199 | golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 200 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 201 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 202 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 203 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 204 | golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= 205 | golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 206 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 207 | golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 208 | golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 209 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 210 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 211 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 212 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 213 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= 214 | golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 215 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 216 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 217 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 218 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 219 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 220 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 221 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 222 | golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 223 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 224 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 225 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 226 | golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= 227 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 228 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 229 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 230 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 231 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 232 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 233 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 234 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 235 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 236 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 237 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 238 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 239 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 246 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 247 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 249 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 260 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 261 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 262 | golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 264 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 266 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 267 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 268 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 269 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 270 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 271 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 272 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 273 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 274 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 275 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 276 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 277 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 278 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 279 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 280 | golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= 281 | golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= 282 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 283 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 284 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 285 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 286 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 287 | golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 288 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 289 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 290 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 291 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 292 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 293 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 294 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 295 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 296 | golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 297 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 298 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 299 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 300 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 301 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 302 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 303 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 304 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 305 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 306 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 307 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 308 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 309 | golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 310 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 311 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 312 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 313 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 314 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 315 | golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= 316 | golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= 317 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 318 | golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 319 | golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= 320 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 321 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 322 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 323 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 324 | golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 325 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 326 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 327 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 328 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 329 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 330 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 331 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 332 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 333 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 334 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 335 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 336 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 337 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 338 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 339 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 340 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 341 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 342 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 343 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 344 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 345 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 346 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 347 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 348 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 349 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 350 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 351 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 352 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 353 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 354 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 355 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 356 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 357 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 358 | k8s.io/api v0.0.0-20191112020540-7f9008e52f64 h1:aAGj+7E+gRYpt4qjByqnh9miBg8VeV4Rzc4pjwa0gmY= 359 | k8s.io/api v0.0.0-20191112020540-7f9008e52f64/go.mod h1:8svLRMiLwQReMTycutfjsaQ0ackWIf8HCT4UcixYLjI= 360 | k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= 361 | k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= 362 | k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= 363 | k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= 364 | k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= 365 | k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= 366 | k8s.io/apimachinery v0.0.0-20191111054156-6eb29fdf75dc h1:hC0UI7qlplCVlRexiPMHwcOCT3IPk9Pgo599vKGOOS4= 367 | k8s.io/apimachinery v0.0.0-20191111054156-6eb29fdf75dc/go.mod h1:+6CX7hP4aLfX2sb91JYDMIp0VqDSog2kZu0BHe+lP+s= 368 | k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= 369 | k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= 370 | k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= 371 | k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 372 | k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= 373 | k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= 374 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 375 | k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= 376 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 377 | k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= 378 | k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 379 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 380 | k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= 381 | k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 382 | k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 383 | k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= 384 | k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= 385 | k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= 386 | k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 387 | k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= 388 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= 389 | k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= 390 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 391 | k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= 392 | k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 393 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= 394 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= 395 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 396 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= 397 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= 398 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= 399 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 400 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 401 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= 402 | sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= 403 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 404 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 405 | -------------------------------------------------------------------------------- /image/helper_functions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/sha256" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/ghodss/yaml" 10 | "github.com/golang/glog" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | func loadConfig(configFile string) (*Config, error) { 15 | data, err := os.ReadFile(configFile) 16 | if err != nil { 17 | return nil, err 18 | } 19 | glog.Infof("New configuration: sha256sum %x", sha256.Sum256(data)) 20 | 21 | var cfg Config 22 | if err := yaml.Unmarshal(data, &cfg); err != nil { 23 | return nil, err 24 | } 25 | glog.Infof("Configuration data: %+v", &cfg) 26 | 27 | return &cfg, nil 28 | } 29 | 30 | // mutationRequired checks whether the target resource needs to be mutated. 31 | // Mutation is enabled by default unless explicitly disabled. 32 | func mutationRequired(ignoredList []string, metadata *metav1.ObjectMeta) bool { 33 | // skip excluded kubernetes system namespaces 34 | for _, namespace := range ignoredList { 35 | if metadata.Namespace == namespace { 36 | glog.Infof("Skip mutation for %v in namespace: %v", metadata.Name, metadata.Namespace) 37 | return false 38 | } 39 | } 40 | 41 | annotations := metadata.GetAnnotations() 42 | if annotations == nil { 43 | annotations = map[string]string{} 44 | } 45 | 46 | status := annotations[admissionWebhookAnnotationStatusKey] 47 | 48 | // determine whether to perform mutation based on annotation for the target resource 49 | var required bool 50 | if strings.ToLower(status) == "injected" { 51 | required = false 52 | } else { 53 | switch strings.ToLower(annotations[admissionWebhookAnnotationInjectKey]) { 54 | default: 55 | required = true 56 | case "n", "no", "false", "off": 57 | required = false 58 | } 59 | } 60 | 61 | glog.Infof("Mutation policy for %v/%v: status: %q required:%v", metadata.Namespace, metadata.Name, status, required) 62 | return required 63 | } 64 | 65 | func updateAnnotation(target map[string]string, annotations map[string]string) (patch []patchOperation) { 66 | for k, v := range annotations { 67 | if target == nil { 68 | target = map[string]string{} 69 | patch = append(patch, patchOperation{ 70 | Op: "add", 71 | Path: "/metadata/annotations", 72 | Value: map[string]string{ 73 | k: v, 74 | }, 75 | }) 76 | } else if target[k] == "" { 77 | target = map[string]string{} 78 | patch = append(patch, patchOperation{ 79 | Op: "add", 80 | Path: "/metadata/annotations/" + k, 81 | Value: v, 82 | }) 83 | } else { 84 | patch = append(patch, patchOperation{ 85 | Op: "replace", 86 | Path: "/metadata/annotations/" + k, 87 | Value: v, 88 | }) 89 | } 90 | } 91 | return patch 92 | } 93 | 94 | // function to test conditions pased in and determine if we need to replace existing config or skip it when it matches 95 | func checkReplaceOrSkip(idx int, inPath string, conditions ...bool) (skip bool, op, path string) { 96 | 97 | for _, condition := range conditions { 98 | if !condition { 99 | op = "replace" 100 | path = fmt.Sprintf("%s/%d", inPath, idx) 101 | skip = false 102 | return 103 | } 104 | } 105 | 106 | // If we reach this point, all conditions are true 107 | skip = true // We skip only if all conditions are true 108 | return 109 | 110 | } 111 | -------------------------------------------------------------------------------- /image/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | 13 | "github.com/golang/glog" 14 | ) 15 | 16 | func main() { 17 | var parameters WhSvrParameters 18 | 19 | // get command line parameters 20 | flag.IntVar(¶meters.port, "port", 443, "Webhook server port.") 21 | flag.StringVar(¶meters.certFile, "tlsCertFile", "/etc/webhook/certs/cert.pem", "File containing the x509 Certificate for HTTPS.") 22 | flag.StringVar(¶meters.keyFile, "tlsKeyFile", "/etc/webhook/certs/key.pem", "File containing the x509 private key to --tlsCertFile.") 23 | flag.StringVar(¶meters.envCfgFile, "envCfgFile", "/etc/webhook/config/envconfig.yaml", "File containing the mutation configuration.") 24 | flag.Parse() 25 | 26 | envConfig, err := loadConfig(parameters.envCfgFile) 27 | if err != nil { 28 | glog.Errorf("Error loading configuration: %v", err) 29 | } 30 | 31 | whsvr := &WebhookServer{ 32 | envConfig: envConfig, 33 | server: &http.Server{ 34 | Addr: fmt.Sprintf(":%v", parameters.port), 35 | TLSConfig: &tls.Config{ 36 | GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { 37 | pair, err := tls.LoadX509KeyPair(parameters.certFile, parameters.keyFile) 38 | if err != nil { 39 | return nil, fmt.Errorf("Error loading key pair: %w", err) 40 | } 41 | 42 | return &pair, nil 43 | }, 44 | }, 45 | }, 46 | } 47 | 48 | // define http server and server handler 49 | mux := http.NewServeMux() 50 | mux.HandleFunc("/mutate", whsvr.serve) 51 | whsvr.server.Handler = mux 52 | 53 | // start webhook server in new rountine 54 | go func() { 55 | if err := whsvr.server.ListenAndServeTLS("", ""); err != nil { 56 | glog.Errorf("Filed to listen and serve env-injector-webhook server: %v", err) 57 | } 58 | }() 59 | 60 | // listening OS shutdown signal 61 | signalChan := make(chan os.Signal, 1) 62 | signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) 63 | <-signalChan 64 | 65 | glog.Infof("Got OS shutdown signal, shutting down env-injector-webhook server...") 66 | whsvr.server.Shutdown(context.Background()) 67 | } 68 | -------------------------------------------------------------------------------- /image/remove_pod_affinity.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // removePodAntiAffinity performs the mutation(s) needed to remove podAntiAffinity 4 | func removePodAntiAffinity(basePath string) (patch []patchOperation) { 5 | patch = append(patch, patchOperation{ 6 | Op: "remove", 7 | Path: basePath, 8 | }) 9 | 10 | return patch 11 | } 12 | -------------------------------------------------------------------------------- /image/test/env_test_1.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | -------------------------------------------------------------------------------- /image/test/env_test_2.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | - name: SUBSCRIPTION 5 | value: subscription-00 6 | -------------------------------------------------------------------------------- /image/test/env_test_3.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | - name: SUBSCRIPTION 5 | value: subscription-00 6 | dnsOptions: 7 | - name: ndots 8 | value: 3 9 | - name: single-request-reopen 10 | - name: use-vc 11 | -------------------------------------------------------------------------------- /image/test/env_test_4.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | - name: SUBSCRIPTION 5 | value: subscription-00 6 | dnsOptions: 7 | - name: ndots 8 | value: 3 9 | - name: single-request-reopen 10 | - name: use-vc 11 | RequiredNodeAffinityTerms: 12 | - matchExpressions: 13 | - key: agentpool 14 | operator: In 15 | values: 16 | - ubuntu18 17 | - ubuntu1804 18 | -------------------------------------------------------------------------------- /image/test/env_test_5.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | - name: SUBSCRIPTION 5 | value: subscription-00 6 | dnsOptions: 7 | - name: ndots 8 | value: 3 9 | - name: single-request-reopen 10 | - name: use-vc 11 | RequiredNodeAffinityTerms: 12 | - matchExpressions: 13 | - key: agentpool 14 | operator: In 15 | values: 16 | - ubuntu18 17 | - ubuntu1804 18 | preferredNodeAffinityTerms: 19 | - weight: 1 20 | preference: 21 | matchExpressions: 22 | - key: kubernetes.azure.com/scalesetpriority 23 | operator: DoesNotExist -------------------------------------------------------------------------------- /image/test/env_test_6.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | - name: SUBSCRIPTION 5 | value: subscription-00 6 | dnsOptions: 7 | - name: ndots 8 | value: 3 9 | - name: single-request-reopen 10 | - name: use-vc 11 | RequiredNodeAffinityTerms: 12 | - matchExpressions: 13 | - key: agentpool 14 | operator: In 15 | values: 16 | - ubuntu18 17 | - ubuntu1804 18 | preferredNodeAffinityTerms: 19 | - weight: 1 20 | preference: 21 | matchExpressions: 22 | - key: kubernetes.azure.com/scalesetpriority 23 | operator: DoesNotExist 24 | tolerations: 25 | - key: kubernetes.azure.com/scalesetpriority 26 | effect: NoSchedule 27 | operator: Equal 28 | value: spot -------------------------------------------------------------------------------- /image/test/env_test_7.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - name: CLUSTER_NAME 3 | value: aks-test-01 4 | - name: SUBSCRIPTION 5 | value: subscription-00 6 | dnsOptions: 7 | - name: ndots 8 | value: 3 9 | - name: single-request-reopen 10 | - name: use-vc 11 | RequiredNodeAffinityTerms: 12 | - matchExpressions: 13 | - key: agentpool 14 | operator: In 15 | values: 16 | - ubuntu18 17 | - ubuntu1804 18 | preferredNodeAffinityTerms: 19 | - weight: 1 20 | preference: 21 | matchExpressions: 22 | - key: kubernetes.azure.com/scalesetpriority 23 | operator: DoesNotExist 24 | tolerations: 25 | - key: kubernetes.azure.com/scalesetpriority 26 | effect: NoSchedule 27 | operator: Equal 28 | value: spot 29 | topologyConstraints: 30 | - maxSkew: 1 31 | topologyKey: topology.kubernetes.io/zone 32 | whenUnsatisfiable: ScheduleAnyway 33 | nodeAffinityPolicy: Honor 34 | nodeTaintsPolicy: Honor 35 | labelSelector: 36 | matchLabels: 37 | app.kubernetes.io/name: test-app 38 | matchLabelKeys: 39 | - pod-template-hash 40 | removePodAntiAffinity: true -------------------------------------------------------------------------------- /image/webhook.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | 9 | "github.com/golang/glog" 10 | v1 "k8s.io/api/admission/v1" 11 | admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 12 | corev1 "k8s.io/api/core/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime" 15 | "k8s.io/apimachinery/pkg/runtime/serializer" 16 | ) 17 | 18 | var ( 19 | runtimeScheme = runtime.NewScheme() 20 | codecs = serializer.NewCodecFactory(runtimeScheme) 21 | deserializer = codecs.UniversalDeserializer() 22 | ) 23 | 24 | var ignoredNamespaces = []string{ 25 | metav1.NamespaceSystem, 26 | metav1.NamespacePublic, 27 | } 28 | 29 | const ( 30 | admissionWebhookAnnotationInjectKey = "env-injector-webhook-inject" 31 | admissionWebhookAnnotationStatusKey = "env-injector-webhook-status" 32 | ) 33 | 34 | type WebhookServer struct { 35 | envConfig *Config 36 | server *http.Server 37 | } 38 | 39 | // Webhook Server parameters 40 | type WhSvrParameters struct { 41 | port int // webhook server port 42 | certFile string // path to the x509 certificate for https 43 | keyFile string // path to the x509 private key matching `CertFile` 44 | envCfgFile string // path to env injector configuration file 45 | } 46 | 47 | type Config struct { 48 | Env []corev1.EnvVar `yaml:"env"` 49 | DnsOptions []corev1.PodDNSConfigOption `yaml:"dnsOptions,omitempty"` 50 | RequiredNodeAffinityTerms []corev1.NodeSelectorTerm `yaml:"requiredNodeAffinityTerms,omitempty"` 51 | PreferredNodeAffinityTerms []corev1.PreferredSchedulingTerm `yaml:"preferredNodeAffinityTerms,omitempty"` 52 | Tolerations []corev1.Toleration `yaml:"tolerations,omitempty"` 53 | TopologyConstraints []corev1.TopologySpreadConstraint `yaml:"topologyConstraints,omitempty"` 54 | RemovePodAntiAffinity bool `yaml:"removePodAntiAffinity,omitempty"` 55 | } 56 | 57 | type patchOperation struct { 58 | Op string `json:"op"` 59 | Path string `json:"path"` 60 | Value interface{} `json:"value,omitempty"` 61 | } 62 | 63 | func init() { 64 | _ = corev1.AddToScheme(runtimeScheme) 65 | _ = admissionregistrationv1.AddToScheme(runtimeScheme) 66 | } 67 | 68 | // main mutation process 69 | func (whsvr *WebhookServer) mutate(ar *v1.AdmissionReview) *v1.AdmissionResponse { 70 | req := ar.Request 71 | var pod corev1.Pod 72 | if err := json.Unmarshal(req.Object.Raw, &pod); err != nil { 73 | glog.Errorf("Could not unmarshal raw object: %v", err) 74 | return &v1.AdmissionResponse{ 75 | Result: &metav1.Status{ 76 | Message: err.Error(), 77 | }, 78 | } 79 | } 80 | 81 | glog.Infof("AdmissionReview for Kind=%v, Namespace=%v Name=%v (%v) UID=%v patchOperation=%v UserInfo=%v", 82 | req.Kind, req.Namespace, req.Name, pod.Name, req.UID, req.Operation, req.UserInfo) 83 | 84 | // determine whether to perform mutation 85 | if !mutationRequired(ignoredNamespaces, &pod.ObjectMeta) { 86 | glog.Infof("Skipping mutation for %s/%s due to policy check", pod.Namespace, pod.Name) 87 | return &v1.AdmissionResponse{ 88 | Allowed: true, 89 | } 90 | } 91 | 92 | annotations := map[string]string{admissionWebhookAnnotationStatusKey: "injected"} 93 | patchBytes, err := createPatch(&pod, whsvr.envConfig, annotations) 94 | if err != nil { 95 | return &v1.AdmissionResponse{ 96 | Result: &metav1.Status{ 97 | Message: err.Error(), 98 | }, 99 | } 100 | } 101 | 102 | glog.Infof("AdmissionResponse: patch=%v\n", string(patchBytes)) 103 | return &v1.AdmissionResponse{ 104 | Allowed: true, 105 | Patch: patchBytes, 106 | PatchType: func() *v1.PatchType { 107 | pt := v1.PatchTypeJSONPatch 108 | return &pt 109 | }(), 110 | } 111 | } 112 | 113 | // serve manages requests to the webhook server 114 | func (whsvr *WebhookServer) serve(w http.ResponseWriter, r *http.Request) { 115 | var body []byte 116 | if r.Body != nil { 117 | if data, err := io.ReadAll(r.Body); err == nil { 118 | body = data 119 | } 120 | } 121 | if len(body) == 0 { 122 | glog.Error("empty body") 123 | http.Error(w, "empty body", http.StatusBadRequest) 124 | return 125 | } 126 | 127 | // verify the content type is accurate 128 | contentType := r.Header.Get("Content-Type") 129 | if contentType != "application/json" { 130 | glog.Errorf("Content-Type=%s, expect application/json", contentType) 131 | http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) 132 | return 133 | } 134 | 135 | var admissionResponse *v1.AdmissionResponse 136 | ar := v1.AdmissionReview{} 137 | if _, _, err := deserializer.Decode(body, nil, &ar); err != nil { 138 | glog.Errorf("Can't decode body: %v", err) 139 | admissionResponse = &v1.AdmissionResponse{ 140 | Result: &metav1.Status{ 141 | Message: err.Error(), 142 | }, 143 | } 144 | } else { 145 | admissionResponse = whsvr.mutate(&ar) 146 | } 147 | 148 | admissionReview := v1.AdmissionReview{} 149 | if admissionResponse != nil { 150 | admissionReview.Response = admissionResponse 151 | if ar.Request != nil { 152 | admissionReview.Response.UID = ar.Request.UID 153 | } 154 | } 155 | 156 | resp, err := json.Marshal(admissionReview) 157 | if err != nil { 158 | glog.Errorf("Can't encode response: %v", err) 159 | http.Error(w, fmt.Sprintf("could not encode response: %v", err), http.StatusInternalServerError) 160 | } 161 | glog.Infof("Ready to write reponse ...") 162 | if _, err := w.Write(resp); err != nil { 163 | glog.Errorf("Can't write response: %v", err) 164 | http.Error(w, fmt.Sprintf("could not write response: %v", err), http.StatusInternalServerError) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /image/webhook_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | ) 10 | 11 | func TestLoadConfig(t *testing.T) { 12 | ndotsVal := "3" 13 | topologyHonorPolicy := corev1.NodeInclusionPolicyHonor 14 | files := []struct { 15 | name string 16 | env *Config 17 | }{ 18 | {"test/env_test_1.yaml", 19 | &Config{ 20 | []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}, 21 | nil, nil, nil, nil, nil, false, // nil = empty tests, false for boolean not defined 22 | }, 23 | }, 24 | {"test/env_test_2.yaml", 25 | &Config{ 26 | []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, 27 | {Name: "SUBSCRIPTION", Value: "subscription-00", ValueFrom: nil}}, 28 | nil, nil, nil, nil, nil, false, // nil = empty tests, false for boolean not defined 29 | }, 30 | }, 31 | {"test/env_test_3.yaml", 32 | &Config{ 33 | []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, 34 | {Name: "SUBSCRIPTION", Value: "subscription-00", ValueFrom: nil}}, 35 | []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, 36 | {Name: "single-request-reopen", Value: nil}, 37 | {Name: "use-vc", Value: nil}}, 38 | nil, nil, nil, nil, false, // nil = empty tests, false for boolean not defined 39 | }, 40 | }, 41 | {"test/env_test_4.yaml", 42 | &Config{ 43 | []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, 44 | {Name: "SUBSCRIPTION", Value: "subscription-00", ValueFrom: nil}}, 45 | []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, 46 | {Name: "single-request-reopen", Value: nil}, 47 | {Name: "use-vc", Value: nil}}, 48 | []corev1.NodeSelectorTerm{{ 49 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 50 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 51 | }}, 52 | }}, 53 | nil, nil, nil, false, // nil = empty tests, false for boolean not defined 54 | }, 55 | }, 56 | {"test/env_test_5.yaml", 57 | &Config{[]corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, 58 | {Name: "SUBSCRIPTION", Value: "subscription-00", ValueFrom: nil}}, 59 | []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, 60 | {Name: "single-request-reopen", Value: nil}, 61 | {Name: "use-vc", Value: nil}}, 62 | []corev1.NodeSelectorTerm{{ 63 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 64 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 65 | }}, 66 | }}, 67 | []corev1.PreferredSchedulingTerm{{ 68 | Weight: 1, 69 | Preference: corev1.NodeSelectorTerm{ 70 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 71 | Key: "kubernetes.azure.com/scalesetpriority", Operator: corev1.NodeSelectorOpDoesNotExist, 72 | }}, 73 | }, 74 | }}, 75 | nil, nil, false, // nil = empty tests, false for boolean not defined 76 | }, 77 | }, 78 | {"test/env_test_6.yaml", 79 | &Config{ 80 | []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, 81 | {Name: "SUBSCRIPTION", Value: "subscription-00", ValueFrom: nil}}, 82 | []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, 83 | {Name: "single-request-reopen", Value: nil}, 84 | {Name: "use-vc", Value: nil}}, 85 | []corev1.NodeSelectorTerm{{MatchExpressions: []corev1.NodeSelectorRequirement{ 86 | {Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}}}}}, 87 | []corev1.PreferredSchedulingTerm{{ 88 | Weight: 1, 89 | Preference: corev1.NodeSelectorTerm{ 90 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 91 | Key: "kubernetes.azure.com/scalesetpriority", Operator: corev1.NodeSelectorOpDoesNotExist, 92 | }}, 93 | }, 94 | }}, 95 | []corev1.Toleration{{ 96 | Key: "kubernetes.azure.com/scalesetpriority", 97 | Effect: "NoSchedule", 98 | Operator: "Equal", 99 | Value: "spot", 100 | }}, 101 | nil, false, // nil = empty tests, false for boolean not defined 102 | }, 103 | }, 104 | {"test/env_test_7.yaml", 105 | &Config{ 106 | []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, 107 | {Name: "SUBSCRIPTION", Value: "subscription-00", ValueFrom: nil}}, 108 | []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, 109 | {Name: "single-request-reopen", Value: nil}, 110 | {Name: "use-vc", Value: nil}}, 111 | []corev1.NodeSelectorTerm{{MatchExpressions: []corev1.NodeSelectorRequirement{ 112 | {Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}}}}}, 113 | []corev1.PreferredSchedulingTerm{{ 114 | Weight: 1, 115 | Preference: corev1.NodeSelectorTerm{ 116 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 117 | Key: "kubernetes.azure.com/scalesetpriority", Operator: corev1.NodeSelectorOpDoesNotExist, 118 | }}, 119 | }, 120 | }}, 121 | []corev1.Toleration{{ 122 | Key: "kubernetes.azure.com/scalesetpriority", 123 | Effect: "NoSchedule", 124 | Operator: "Equal", 125 | Value: "spot", 126 | }}, 127 | []corev1.TopologySpreadConstraint{{ 128 | MaxSkew: 1, 129 | TopologyKey: "topology.kubernetes.io/zone", 130 | NodeAffinityPolicy: &topologyHonorPolicy, 131 | NodeTaintsPolicy: &topologyHonorPolicy, 132 | WhenUnsatisfiable: "ScheduleAnyway", 133 | LabelSelector: &metav1.LabelSelector{ 134 | MatchLabels: map[string]string{ 135 | "app.kubernetes.io/name": "test-app", 136 | }, 137 | }, 138 | MatchLabelKeys: []string{ 139 | "pod-template-hash", 140 | }, 141 | }}, 142 | true, 143 | }, 144 | }, 145 | } 146 | 147 | for _, f := range files { 148 | config, err := loadConfig(f.name) 149 | if err != nil { 150 | t.Errorf("Error loading file %s", f.name) 151 | t.Fatal(err) 152 | } 153 | if !cmp.Equal(config, f.env) { 154 | t.Errorf("loadConfig was incorrect, got: %v, want: %v.", config, f.env) 155 | } 156 | } 157 | } 158 | 159 | func TestMutationRequired(t *testing.T) { 160 | metas := []struct { 161 | ignoredList []string 162 | metadata *metav1.ObjectMeta 163 | required bool 164 | }{ 165 | {[]string{"admin", "kube-system"}, 166 | &metav1.ObjectMeta{Namespace: "admin", Annotations: map[string]string{}}, 167 | false}, 168 | {[]string{"admin", "kube-system"}, 169 | &metav1.ObjectMeta{Namespace: "rpe", Annotations: map[string]string{"some-other-annotation/inject": "false"}}, 170 | true}, 171 | {[]string{"admin", "kube-system"}, 172 | &metav1.ObjectMeta{Namespace: "rpe", Annotations: map[string]string{admissionWebhookAnnotationStatusKey: "injected"}}, 173 | false}, 174 | {[]string{"admin", "kube-system"}, 175 | &metav1.ObjectMeta{Namespace: "rpe", Annotations: map[string]string{admissionWebhookAnnotationInjectKey: "false"}}, 176 | false}, 177 | {[]string{"admin", "kube-system"}, 178 | &metav1.ObjectMeta{Namespace: "rpe", Annotations: map[string]string{}}, 179 | true}, 180 | {[]string{"admin", "kube-system"}, 181 | &metav1.ObjectMeta{Namespace: "rpe", Annotations: map[string]string{admissionWebhookAnnotationInjectKey: "true"}}, 182 | true}, 183 | } 184 | 185 | for _, m := range metas { 186 | required := mutationRequired(m.ignoredList, m.metadata) 187 | if required != m.required { 188 | t.Errorf("mutationRequired was incorrect, for %v, got: %t, want: %t.", m, required, m.required) 189 | } 190 | } 191 | } 192 | 193 | func TestAddEnv(t *testing.T) { 194 | envs := []struct { 195 | targetEnv []corev1.EnvVar 196 | sourceEnv []corev1.EnvVar 197 | path string 198 | patch []patchOperation 199 | }{ 200 | { 201 | targetEnv: []corev1.EnvVar{{Name: "ENV_TEST_NAME", Value: "env-test-value", ValueFrom: nil}}, 202 | sourceEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}, 203 | path: "/spec/containers/nginx/env", 204 | patch: []patchOperation{ 205 | {Op: "add", Path: "/spec/containers/nginx/env/-", Value: corev1.EnvVar{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}, 206 | }, 207 | }, 208 | { 209 | targetEnv: []corev1.EnvVar{}, 210 | sourceEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}, 211 | path: "/spec/containers/nginx/env", 212 | patch: []patchOperation{ 213 | {Op: "add", Path: "/spec/containers/nginx/env", Value: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}}, 214 | }, 215 | }, 216 | { 217 | targetEnv: []corev1.EnvVar{{Name: "ENV_TEST_NAME", Value: "env-test-value", ValueFrom: nil}}, 218 | sourceEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, {Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 219 | path: "/spec/containers/nginx/env", 220 | patch: []patchOperation{ 221 | {Op: "add", Path: "/spec/containers/nginx/env/-", Value: corev1.EnvVar{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}, 222 | {Op: "add", Path: "/spec/containers/nginx/env/-", Value: corev1.EnvVar{Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 223 | }, 224 | }, 225 | { 226 | targetEnv: nil, 227 | sourceEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, {Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 228 | path: "/spec/containers/nginx/env", 229 | patch: []patchOperation{ 230 | {Op: "add", Path: "/spec/containers/nginx/env", Value: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}}}, 231 | {Op: "add", Path: "/spec/containers/nginx/env/-", Value: corev1.EnvVar{Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 232 | }, 233 | }, 234 | { 235 | targetEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, {Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 236 | sourceEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, {Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 237 | path: "/spec/containers/nginx/env", 238 | patch: []patchOperation{}, 239 | }, 240 | { 241 | targetEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-01", ValueFrom: nil}, {Name: "SUBSCRIPTION", Value: "subscription-01", ValueFrom: nil}}, 242 | sourceEnv: []corev1.EnvVar{{Name: "CLUSTER_NAME", Value: "aks-test-02", ValueFrom: nil}, {Name: "SUBSCRIPTION", Value: "subscription-02", ValueFrom: nil}}, 243 | path: "/spec/containers/nginx/env", 244 | patch: []patchOperation{ 245 | {Op: "replace", Path: "/spec/containers/nginx/env/0", Value: corev1.EnvVar{Name: "CLUSTER_NAME", Value: "aks-test-02", ValueFrom: nil}}, 246 | {Op: "replace", Path: "/spec/containers/nginx/env/1", Value: corev1.EnvVar{Name: "SUBSCRIPTION", Value: "subscription-02", ValueFrom: nil}}, 247 | }, 248 | }, 249 | } 250 | 251 | for _, e := range envs { 252 | patch := addEnv(e.targetEnv, e.sourceEnv, e.path) 253 | if !cmp.Equal(patch, e.patch) { 254 | t.Errorf("addEnv was incorrect, for %v, got: %v, want: %v.", e.targetEnv, patch, e.patch) 255 | } 256 | } 257 | } 258 | 259 | func TestAddDnsOptions(t *testing.T) { 260 | ndotsVal := "3" 261 | ndotsValOld := "5" 262 | envs := []struct { 263 | targetOptions []corev1.PodDNSConfigOption 264 | sourceOptions []corev1.PodDNSConfigOption 265 | path string 266 | patch []patchOperation 267 | }{ 268 | { 269 | targetOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}}, 270 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "single-request-reopen", Value: nil}}, 271 | path: "/spec/dnsConfig/options", 272 | patch: []patchOperation{{Op: "add", Path: "/spec/dnsConfig/options/-", Value: corev1.PodDNSConfigOption{Name: "single-request-reopen", Value: nil}}}, 273 | }, 274 | { 275 | targetOptions: []corev1.PodDNSConfigOption{}, 276 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}}, 277 | path: "/spec/dnsConfig/options", 278 | patch: []patchOperation{{Op: "add", Path: "/spec/dnsConfig/options", Value: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}}}}, 279 | }, 280 | { 281 | targetOptions: []corev1.PodDNSConfigOption{{Name: "single-request-reopen", Value: nil}}, 282 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, {Name: "use-vc", Value: nil}}, 283 | path: "/spec/dnsConfig/options", 284 | patch: []patchOperation{ 285 | {Op: "add", Path: "/spec/dnsConfig/options/-", Value: corev1.PodDNSConfigOption{Name: "ndots", Value: &ndotsVal}}, 286 | {Op: "add", Path: "/spec/dnsConfig/options/-", Value: corev1.PodDNSConfigOption{Name: "use-vc", Value: nil}}, 287 | }, 288 | }, 289 | { 290 | targetOptions: nil, 291 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, {Name: "use-vc", Value: nil}}, 292 | path: "/spec/dnsConfig/options", 293 | patch: []patchOperation{ 294 | {Op: "add", Path: "/spec/dnsConfig/options", Value: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}}}, 295 | {Op: "add", Path: "/spec/dnsConfig/options/-", Value: corev1.PodDNSConfigOption{Name: "use-vc", Value: nil}}, 296 | }, 297 | }, 298 | { 299 | targetOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsValOld}}, 300 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, {Name: "use-vc", Value: nil}}, 301 | path: "/spec/dnsConfig/options", 302 | patch: []patchOperation{ 303 | {Op: "replace", Path: "/spec/dnsConfig/options/0", Value: corev1.PodDNSConfigOption{Name: "ndots", Value: &ndotsVal}}, 304 | {Op: "add", Path: "/spec/dnsConfig/options/-", Value: corev1.PodDNSConfigOption{Name: "use-vc", Value: nil}}, 305 | }, 306 | }, 307 | { 308 | targetOptions: []corev1.PodDNSConfigOption{{Name: "single-request-reopen", Value: nil}, {Name: "ndots", Value: &ndotsValOld}}, 309 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, {Name: "use-vc", Value: nil}}, 310 | path: "/spec/dnsConfig/options", 311 | patch: []patchOperation{ 312 | {Op: "replace", Path: "/spec/dnsConfig/options/1", Value: corev1.PodDNSConfigOption{Name: "ndots", Value: &ndotsVal}}, 313 | {Op: "add", Path: "/spec/dnsConfig/options/-", Value: corev1.PodDNSConfigOption{Name: "use-vc", Value: nil}}, 314 | }, 315 | }, 316 | { 317 | targetOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, {Name: "use-vc", Value: nil}}, 318 | sourceOptions: []corev1.PodDNSConfigOption{{Name: "ndots", Value: &ndotsVal}, {Name: "use-vc", Value: nil}}, 319 | path: "/spec/dnsConfig/options", 320 | patch: []patchOperation{}, 321 | }, 322 | } 323 | for _, e := range envs { 324 | patch := addDnsOptions(e.targetOptions, e.sourceOptions, e.path) 325 | if !cmp.Equal(patch, e.patch) { 326 | t.Errorf("addDnsOptions was incorrect, for %v, got: %v, want: %v.", e.targetOptions, patch, e.patch) 327 | } 328 | } 329 | 330 | } 331 | 332 | func TestAddRequiredNodeAffinity(t *testing.T) { 333 | envs := []struct { 334 | targetTerms []corev1.NodeSelectorTerm 335 | sourceTerms []corev1.NodeSelectorTerm 336 | path string 337 | patch []patchOperation 338 | }{ 339 | { 340 | targetTerms: []corev1.NodeSelectorTerm{}, 341 | sourceTerms: []corev1.NodeSelectorTerm{{ 342 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 343 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 344 | }}, 345 | }}, 346 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 347 | patch: []patchOperation{{ 348 | Op: "add", 349 | Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 350 | Value: []corev1.NodeSelectorTerm{{ 351 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 352 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 353 | }}, 354 | }}, 355 | }}, 356 | }, 357 | { 358 | targetTerms: []corev1.NodeSelectorTerm{}, 359 | sourceTerms: []corev1.NodeSelectorTerm{{ 360 | MatchFields: []corev1.NodeSelectorRequirement{{ 361 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 362 | }}, 363 | }}, 364 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 365 | patch: []patchOperation{{ 366 | Op: "add", 367 | Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 368 | Value: []corev1.NodeSelectorTerm{{ 369 | MatchFields: []corev1.NodeSelectorRequirement{{ 370 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 371 | }}, 372 | }}, 373 | }}, 374 | }, 375 | { 376 | targetTerms: []corev1.NodeSelectorTerm{{ 377 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 378 | Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"uksouth"}, 379 | }}, 380 | }}, 381 | sourceTerms: []corev1.NodeSelectorTerm{{ 382 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 383 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 384 | }}, 385 | }}, 386 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 387 | patch: []patchOperation{{ 388 | Op: "add", Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/-", 389 | Value: corev1.NodeSelectorTerm{ 390 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 391 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 392 | }}, 393 | }, 394 | }}, 395 | }, 396 | { 397 | targetTerms: []corev1.NodeSelectorTerm{{ 398 | MatchFields: []corev1.NodeSelectorRequirement{{ 399 | Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"uksouth"}, 400 | }}, 401 | }}, 402 | sourceTerms: []corev1.NodeSelectorTerm{{ 403 | MatchFields: []corev1.NodeSelectorRequirement{{ 404 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 405 | }}, 406 | }}, 407 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 408 | patch: []patchOperation{{ 409 | Op: "add", Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/-", 410 | Value: corev1.NodeSelectorTerm{ 411 | MatchFields: []corev1.NodeSelectorRequirement{{ 412 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 413 | }}, 414 | }, 415 | }}, 416 | }, 417 | { 418 | targetTerms: []corev1.NodeSelectorTerm{{ 419 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 420 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 421 | }}, 422 | }}, 423 | sourceTerms: []corev1.NodeSelectorTerm{{ 424 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 425 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 426 | }}, 427 | }}, 428 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 429 | patch: []patchOperation{}, 430 | }, 431 | { 432 | targetTerms: []corev1.NodeSelectorTerm{{ 433 | MatchFields: []corev1.NodeSelectorRequirement{{ 434 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 435 | }}, 436 | }}, 437 | sourceTerms: []corev1.NodeSelectorTerm{{ 438 | MatchFields: []corev1.NodeSelectorRequirement{{ 439 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 440 | }}, 441 | }}, 442 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 443 | patch: []patchOperation{}, 444 | }, 445 | { 446 | targetTerms: []corev1.NodeSelectorTerm{{ 447 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 448 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 449 | }}, 450 | }}, 451 | sourceTerms: []corev1.NodeSelectorTerm{{ 452 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 453 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu20", "ubuntu1804"}, 454 | }}, 455 | }}, 456 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 457 | patch: []patchOperation{{ 458 | Op: "replace", 459 | Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/0", 460 | Value: corev1.NodeSelectorTerm{ 461 | MatchExpressions: []corev1.NodeSelectorRequirement{{ 462 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu20", "ubuntu1804"}, 463 | }}, 464 | }, 465 | }}, 466 | }, 467 | { 468 | targetTerms: []corev1.NodeSelectorTerm{{ 469 | MatchFields: []corev1.NodeSelectorRequirement{{ 470 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 471 | }}, 472 | }}, 473 | sourceTerms: []corev1.NodeSelectorTerm{{ 474 | MatchFields: []corev1.NodeSelectorRequirement{{ 475 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu2004"}, 476 | }}, 477 | }}, 478 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 479 | patch: []patchOperation{{ 480 | Op: "replace", 481 | Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/0", 482 | Value: corev1.NodeSelectorTerm{ 483 | MatchFields: []corev1.NodeSelectorRequirement{{ 484 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu2004"}, 485 | }, 486 | }}, 487 | }}, 488 | }, 489 | { 490 | targetTerms: []corev1.NodeSelectorTerm{{ 491 | MatchFields: []corev1.NodeSelectorRequirement{{ 492 | Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"A", "B"}, 493 | }}, 494 | }, { 495 | MatchFields: []corev1.NodeSelectorRequirement{{ 496 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 497 | }}, 498 | }}, 499 | sourceTerms: []corev1.NodeSelectorTerm{{ 500 | MatchFields: []corev1.NodeSelectorRequirement{{ 501 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu2004"}, 502 | }}, 503 | }}, 504 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 505 | patch: []patchOperation{{ 506 | Op: "replace", 507 | Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/1", 508 | Value: corev1.NodeSelectorTerm{ 509 | MatchFields: []corev1.NodeSelectorRequirement{{ 510 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu2004"}, 511 | }}, 512 | }, 513 | }}, 514 | }, 515 | { 516 | targetTerms: []corev1.NodeSelectorTerm{{ 517 | MatchFields: []corev1.NodeSelectorRequirement{{ 518 | Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"A", "B"}, 519 | }}, 520 | }, { 521 | MatchFields: []corev1.NodeSelectorRequirement{{ 522 | Key: "agentpool", Operator: corev1.NodeSelectorOpIn, Values: []string{"ubuntu18", "ubuntu1804"}, 523 | }}, 524 | }}, 525 | sourceTerms: []corev1.NodeSelectorTerm{{ 526 | MatchFields: []corev1.NodeSelectorRequirement{{ 527 | Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"A", "B", "C"}, 528 | }}, 529 | }}, 530 | path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution", 531 | patch: []patchOperation{{ 532 | Op: "replace", 533 | Path: "/spec/affinity/nodeAffinity/requiredDuringSchedulingIgnoredDuringExecution/0", 534 | Value: corev1.NodeSelectorTerm{ 535 | MatchFields: []corev1.NodeSelectorRequirement{{ 536 | Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"A", "B", "C"}, 537 | }}, 538 | }, 539 | }}, 540 | }, 541 | } 542 | for _, e := range envs { 543 | patch := addRequiredNodeAffinityTerms(e.targetTerms, e.sourceTerms, e.path) 544 | if !cmp.Equal(patch, e.patch) { 545 | t.Errorf("addRequiredNodeAffinityTerms was incorrect, for %v, got: %v, want: %v.", e.targetTerms, patch, e.patch) 546 | } 547 | } 548 | } 549 | 550 | func TestAddPreferredNodeAffinity(t *testing.T) { 551 | envs := []struct { 552 | targetTerms []corev1.PreferredSchedulingTerm 553 | sourceTerms []corev1.PreferredSchedulingTerm 554 | path string 555 | patch []patchOperation 556 | }{ 557 | { 558 | targetTerms: []corev1.PreferredSchedulingTerm{}, 559 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 560 | Weight: 1, 561 | Preference: corev1.NodeSelectorTerm{ 562 | MatchExpressions: []corev1.NodeSelectorRequirement{ 563 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 564 | }, 565 | }, 566 | }}, 567 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 568 | patch: []patchOperation{{ 569 | Op: "add", 570 | Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 571 | Value: []corev1.PreferredSchedulingTerm{{ 572 | Weight: 1, 573 | Preference: corev1.NodeSelectorTerm{ 574 | MatchExpressions: []corev1.NodeSelectorRequirement{ 575 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 576 | }, 577 | }, 578 | }}, 579 | }}, 580 | }, 581 | { 582 | targetTerms: []corev1.PreferredSchedulingTerm{}, 583 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 584 | Weight: 1, 585 | Preference: corev1.NodeSelectorTerm{ 586 | MatchFields: []corev1.NodeSelectorRequirement{ 587 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 588 | }, 589 | }, 590 | }}, 591 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 592 | patch: []patchOperation{{ 593 | Op: "add", 594 | Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 595 | Value: []corev1.PreferredSchedulingTerm{{ 596 | Weight: 1, 597 | Preference: corev1.NodeSelectorTerm{ 598 | MatchFields: []corev1.NodeSelectorRequirement{ 599 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 600 | }, 601 | }, 602 | }}, 603 | }}, 604 | }, 605 | { 606 | targetTerms: []corev1.PreferredSchedulingTerm{{ 607 | Weight: 1, 608 | Preference: corev1.NodeSelectorTerm{ 609 | MatchExpressions: []corev1.NodeSelectorRequirement{ 610 | {Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"uksouth"}}, 611 | }, 612 | }, 613 | }}, 614 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 615 | Weight: 1, 616 | Preference: corev1.NodeSelectorTerm{ 617 | MatchExpressions: []corev1.NodeSelectorRequirement{ 618 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 619 | }, 620 | }, 621 | }}, 622 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 623 | patch: []patchOperation{{ 624 | Op: "add", 625 | Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution/-", 626 | Value: corev1.PreferredSchedulingTerm{ 627 | Weight: 1, 628 | Preference: corev1.NodeSelectorTerm{ 629 | MatchExpressions: []corev1.NodeSelectorRequirement{ 630 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 631 | }, 632 | }, 633 | }, 634 | }}, 635 | }, 636 | { 637 | targetTerms: []corev1.PreferredSchedulingTerm{{ 638 | Weight: 1, 639 | Preference: corev1.NodeSelectorTerm{ 640 | MatchFields: []corev1.NodeSelectorRequirement{ 641 | {Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"uksouth"}}, 642 | }, 643 | }, 644 | }}, 645 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 646 | Weight: 1, 647 | Preference: corev1.NodeSelectorTerm{ 648 | MatchFields: []corev1.NodeSelectorRequirement{ 649 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 650 | }, 651 | }, 652 | }}, 653 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 654 | patch: []patchOperation{{ 655 | Op: "add", 656 | Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution/-", 657 | Value: corev1.PreferredSchedulingTerm{ 658 | Weight: 1, 659 | Preference: corev1.NodeSelectorTerm{ 660 | MatchFields: []corev1.NodeSelectorRequirement{ 661 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 662 | }, 663 | }, 664 | }, 665 | }}, 666 | }, 667 | { 668 | targetTerms: []corev1.PreferredSchedulingTerm{{ 669 | Weight: 1, 670 | Preference: corev1.NodeSelectorTerm{ 671 | MatchExpressions: []corev1.NodeSelectorRequirement{ 672 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 673 | }, 674 | }, 675 | }}, 676 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 677 | Weight: 1, 678 | Preference: corev1.NodeSelectorTerm{ 679 | MatchExpressions: []corev1.NodeSelectorRequirement{ 680 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 681 | }, 682 | }, 683 | }}, 684 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 685 | patch: []patchOperation{}, 686 | }, 687 | { 688 | targetTerms: []corev1.PreferredSchedulingTerm{{ 689 | Weight: 1, 690 | Preference: corev1.NodeSelectorTerm{ 691 | MatchFields: []corev1.NodeSelectorRequirement{ 692 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 693 | }, 694 | }, 695 | }}, 696 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 697 | Weight: 1, 698 | Preference: corev1.NodeSelectorTerm{ 699 | MatchFields: []corev1.NodeSelectorRequirement{ 700 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 701 | }, 702 | }, 703 | }}, 704 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 705 | patch: []patchOperation{}, 706 | }, 707 | { 708 | targetTerms: []corev1.PreferredSchedulingTerm{{ 709 | Weight: 1, 710 | Preference: corev1.NodeSelectorTerm{ 711 | MatchFields: []corev1.NodeSelectorRequirement{ 712 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 713 | }, 714 | }, 715 | }}, 716 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 717 | Weight: 1, 718 | Preference: corev1.NodeSelectorTerm{ 719 | MatchFields: []corev1.NodeSelectorRequirement{ 720 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"hdd"}}, 721 | }, 722 | }, 723 | }}, 724 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 725 | patch: []patchOperation{{ 726 | Op: "replace", 727 | Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution/0", 728 | Value: corev1.PreferredSchedulingTerm{ 729 | Weight: 1, 730 | Preference: corev1.NodeSelectorTerm{ 731 | MatchFields: []corev1.NodeSelectorRequirement{ 732 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"hdd"}}, 733 | }, 734 | }, 735 | }, 736 | }}, 737 | }, 738 | { 739 | targetTerms: []corev1.PreferredSchedulingTerm{{ 740 | Weight: 1, 741 | Preference: corev1.NodeSelectorTerm{ 742 | MatchFields: []corev1.NodeSelectorRequirement{ 743 | {Key: "zone", Operator: corev1.NodeSelectorOpIn, Values: []string{"uksouth"}}, 744 | }, 745 | }, 746 | }, { 747 | Weight: 1, 748 | Preference: corev1.NodeSelectorTerm{ 749 | MatchFields: []corev1.NodeSelectorRequirement{ 750 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"ssd"}}, 751 | }, 752 | }, 753 | }}, 754 | sourceTerms: []corev1.PreferredSchedulingTerm{{ 755 | Weight: 1, 756 | Preference: corev1.NodeSelectorTerm{ 757 | MatchFields: []corev1.NodeSelectorRequirement{ 758 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"hdd"}}, 759 | }, 760 | }, 761 | }}, 762 | path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution", 763 | patch: []patchOperation{{ 764 | Op: "replace", 765 | Path: "/spec/affinity/nodeAffinity/preferredDuringSchedulingIgnoredDuringExecution/1", 766 | Value: corev1.PreferredSchedulingTerm{ 767 | Weight: 1, 768 | Preference: corev1.NodeSelectorTerm{ 769 | MatchFields: []corev1.NodeSelectorRequirement{ 770 | {Key: "disktype", Operator: corev1.NodeSelectorOpIn, Values: []string{"hdd"}}, 771 | }, 772 | }, 773 | }, 774 | }}, 775 | }, 776 | } 777 | 778 | for _, e := range envs { 779 | patch := addPreferredNodeAffinityTerms(e.targetTerms, e.sourceTerms, e.path) 780 | if !cmp.Equal(patch, e.patch) { 781 | t.Errorf("addPreferredNodeAffinityTerms was incorrect, for %v, got: %v, want: %v.", e.targetTerms, patch, e.patch) 782 | } 783 | } 784 | } 785 | 786 | func TestAddTolerations(t *testing.T) { 787 | envs := []struct { 788 | targetTolerations []corev1.Toleration 789 | sourceTolerations []corev1.Toleration 790 | path string 791 | patch []patchOperation 792 | }{ 793 | { 794 | targetTolerations: []corev1.Toleration{}, 795 | sourceTolerations: []corev1.Toleration{{ 796 | Key: "topology.kubernetes.io/region", 797 | Operator: corev1.TolerationOpExists, 798 | Effect: corev1.TaintEffectNoSchedule, 799 | }}, 800 | path: "/spec/tolerations", 801 | patch: []patchOperation{{ 802 | Op: "add", 803 | Path: "/spec/tolerations", 804 | Value: []corev1.Toleration{{ 805 | Key: "topology.kubernetes.io/region", 806 | Operator: corev1.TolerationOpExists, 807 | Effect: corev1.TaintEffectNoSchedule, 808 | }}, 809 | }}, 810 | }, 811 | { 812 | targetTolerations: []corev1.Toleration{{ 813 | Key: "topology.kubernetes.io/region", 814 | Operator: corev1.TolerationOpEqual, 815 | Value: "uksouth", 816 | Effect: corev1.TaintEffectPreferNoSchedule, 817 | }}, 818 | sourceTolerations: []corev1.Toleration{{ 819 | Key: "kubernetes.io/os", 820 | Operator: corev1.TolerationOpEqual, 821 | Value: "Windows", 822 | Effect: corev1.TaintEffectPreferNoSchedule, 823 | }}, 824 | path: "/spec/tolerations", 825 | patch: []patchOperation{{ 826 | Op: "add", 827 | Path: "/spec/tolerations/-", 828 | Value: corev1.Toleration{ 829 | Key: "kubernetes.io/os", 830 | Operator: corev1.TolerationOpEqual, 831 | Value: "Windows", 832 | Effect: corev1.TaintEffectPreferNoSchedule, 833 | }, 834 | }}, 835 | }, 836 | { 837 | targetTolerations: []corev1.Toleration{{ 838 | Key: "kubernetes.io/os", 839 | Operator: corev1.TolerationOpEqual, 840 | Value: "Windows", 841 | Effect: corev1.TaintEffectPreferNoSchedule, 842 | }}, 843 | sourceTolerations: []corev1.Toleration{{ 844 | Key: "kubernetes.io/os", 845 | Operator: corev1.TolerationOpEqual, 846 | Value: "Windows", 847 | Effect: corev1.TaintEffectPreferNoSchedule, 848 | }}, 849 | path: "/spec/tolerations", 850 | patch: []patchOperation{}, 851 | }, 852 | { 853 | targetTolerations: []corev1.Toleration{{ 854 | Key: "kubernetes.io/os", 855 | Operator: corev1.TolerationOpEqual, 856 | Value: "Windows", 857 | Effect: corev1.TaintEffectPreferNoSchedule, 858 | }}, 859 | sourceTolerations: []corev1.Toleration{{ 860 | Key: "kubernetes.io/os", 861 | Operator: corev1.TolerationOpEqual, 862 | Value: "Linux", 863 | Effect: corev1.TaintEffectPreferNoSchedule, 864 | }}, 865 | path: "/spec/tolerations", 866 | patch: []patchOperation{{ 867 | Op: "replace", 868 | Path: "/spec/tolerations/0", 869 | Value: corev1.Toleration{ 870 | Key: "kubernetes.io/os", 871 | Operator: corev1.TolerationOpEqual, 872 | Value: "Linux", 873 | Effect: corev1.TaintEffectPreferNoSchedule, 874 | }, 875 | }}, 876 | }, 877 | { 878 | targetTolerations: []corev1.Toleration{{ 879 | Key: "topology.kubernetes.io/region", 880 | Operator: corev1.TolerationOpExists, 881 | Effect: corev1.TaintEffectNoSchedule, 882 | }, { 883 | Key: "kubernetes.io/os", 884 | Operator: corev1.TolerationOpEqual, 885 | Value: "Windows", 886 | Effect: corev1.TaintEffectPreferNoSchedule, 887 | }}, 888 | sourceTolerations: []corev1.Toleration{{ 889 | Key: "kubernetes.io/os", 890 | Operator: corev1.TolerationOpEqual, 891 | Value: "Linux", 892 | Effect: corev1.TaintEffectPreferNoSchedule, 893 | }}, 894 | path: "/spec/tolerations", 895 | patch: []patchOperation{{ 896 | Op: "replace", 897 | Path: "/spec/tolerations/1", 898 | Value: corev1.Toleration{ 899 | Key: "kubernetes.io/os", 900 | Operator: corev1.TolerationOpEqual, 901 | Value: "Linux", 902 | Effect: corev1.TaintEffectPreferNoSchedule, 903 | }, 904 | }}, 905 | }, 906 | } 907 | for _, e := range envs { 908 | patch := addTolerations(e.targetTolerations, e.sourceTolerations, e.path) 909 | if !cmp.Equal(patch, e.patch) { 910 | t.Errorf("addTolerations was incorrect, for %v, got: %v, want: %v.", e.targetTolerations, patch, e.patch) 911 | } 912 | } 913 | } 914 | 915 | func TestAddTopologySpreadConstraints(t *testing.T) { 916 | topologyHonorPolicy := corev1.NodeInclusionPolicyHonor 917 | envs := []struct { 918 | targetTopologySpreadConstraints []corev1.TopologySpreadConstraint 919 | sourceTopologySpreadConstraints []corev1.TopologySpreadConstraint 920 | path string 921 | patch []patchOperation 922 | }{ 923 | { 924 | targetTopologySpreadConstraints: []corev1.TopologySpreadConstraint{}, 925 | sourceTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 926 | MaxSkew: 1, 927 | TopologyKey: "topology.kubernetes.io/zone", 928 | WhenUnsatisfiable: "ScheduleAnyway", 929 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 930 | NodeAffinityPolicy: &topologyHonorPolicy, 931 | NodeTaintsPolicy: &topologyHonorPolicy, 932 | MatchLabelKeys: []string{"pod-template-hash"}, 933 | }}, 934 | path: "/spec/topologySpreadConstraints", 935 | patch: []patchOperation{{ 936 | Op: "add", 937 | Path: "/spec/topologySpreadConstraints", 938 | Value: []corev1.TopologySpreadConstraint{{ 939 | MaxSkew: 1, 940 | TopologyKey: "topology.kubernetes.io/zone", 941 | WhenUnsatisfiable: "ScheduleAnyway", 942 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 943 | NodeAffinityPolicy: &topologyHonorPolicy, 944 | NodeTaintsPolicy: &topologyHonorPolicy, 945 | MatchLabelKeys: []string{"pod-template-hash"}, 946 | }}, 947 | }}, 948 | }, 949 | { 950 | targetTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 951 | MaxSkew: 1, 952 | TopologyKey: "kubernetes.azure.com/agentpool", 953 | WhenUnsatisfiable: "DoNotSchedule", 954 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "myFirstTestapp"}}, 955 | NodeAffinityPolicy: &topologyHonorPolicy, 956 | NodeTaintsPolicy: &topologyHonorPolicy, 957 | MatchLabelKeys: []string{"pod-template-hash"}, 958 | }}, 959 | sourceTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 960 | MaxSkew: 2, 961 | TopologyKey: "topology.kubernetes.io/zone", 962 | WhenUnsatisfiable: "ScheduleAnyway", 963 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 964 | NodeAffinityPolicy: &topologyHonorPolicy, 965 | NodeTaintsPolicy: &topologyHonorPolicy, 966 | MatchLabelKeys: []string{"pod-template-hash"}, 967 | }}, 968 | path: "/spec/topologySpreadConstraints", 969 | patch: []patchOperation{{ 970 | Op: "add", 971 | Path: "/spec/topologySpreadConstraints/-", 972 | Value: corev1.TopologySpreadConstraint{ 973 | MaxSkew: 2, 974 | TopologyKey: "topology.kubernetes.io/zone", 975 | WhenUnsatisfiable: "ScheduleAnyway", 976 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 977 | NodeAffinityPolicy: &topologyHonorPolicy, 978 | NodeTaintsPolicy: &topologyHonorPolicy, 979 | MatchLabelKeys: []string{"pod-template-hash"}, 980 | }, 981 | }}, 982 | }, 983 | { 984 | targetTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 985 | MaxSkew: 1, 986 | TopologyKey: "topology.kubernetes.io/zone", 987 | WhenUnsatisfiable: "ScheduleAnyway", 988 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 989 | NodeAffinityPolicy: &topologyHonorPolicy, 990 | NodeTaintsPolicy: &topologyHonorPolicy, 991 | MatchLabelKeys: []string{"pod-template-hash"}, 992 | }}, 993 | sourceTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 994 | MaxSkew: 1, 995 | TopologyKey: "topology.kubernetes.io/zone", 996 | WhenUnsatisfiable: "ScheduleAnyway", 997 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 998 | NodeAffinityPolicy: &topologyHonorPolicy, 999 | NodeTaintsPolicy: &topologyHonorPolicy, 1000 | MatchLabelKeys: []string{"pod-template-hash"}, 1001 | }}, 1002 | path: "/spec/topologySpreadConstraints", 1003 | patch: []patchOperation{}, 1004 | }, 1005 | { 1006 | targetTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 1007 | MaxSkew: 1, 1008 | TopologyKey: "topology.kubernetes.io/zone", 1009 | WhenUnsatisfiable: "ScheduleAnyway", 1010 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 1011 | NodeAffinityPolicy: &topologyHonorPolicy, 1012 | NodeTaintsPolicy: &topologyHonorPolicy, 1013 | MatchLabelKeys: []string{"pod-template-hash"}, 1014 | }}, 1015 | sourceTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 1016 | MaxSkew: 2, 1017 | TopologyKey: "topology.kubernetes.io/zone", 1018 | WhenUnsatisfiable: "ScheduleAnyway", 1019 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test"}}, 1020 | NodeAffinityPolicy: &topologyHonorPolicy, 1021 | NodeTaintsPolicy: &topologyHonorPolicy, 1022 | MatchLabelKeys: []string{"pod-template-hash"}, 1023 | }}, 1024 | path: "/spec/topologySpreadConstraints", 1025 | patch: []patchOperation{{ 1026 | Op: "replace", 1027 | Path: "/spec/topologySpreadConstraints/0", 1028 | Value: corev1.TopologySpreadConstraint{ 1029 | MaxSkew: 2, 1030 | TopologyKey: "topology.kubernetes.io/zone", 1031 | WhenUnsatisfiable: "ScheduleAnyway", 1032 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test"}}, 1033 | NodeAffinityPolicy: &topologyHonorPolicy, 1034 | NodeTaintsPolicy: &topologyHonorPolicy, 1035 | MatchLabelKeys: []string{"pod-template-hash"}, 1036 | }, 1037 | }}, 1038 | }, 1039 | { 1040 | targetTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 1041 | MaxSkew: 1, 1042 | TopologyKey: "kubernetes.azure.com/agentpool", 1043 | WhenUnsatisfiable: "DoNotSchedule", 1044 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "myFirstTestapp"}}, 1045 | NodeAffinityPolicy: &topologyHonorPolicy, 1046 | NodeTaintsPolicy: &topologyHonorPolicy, 1047 | MatchLabelKeys: []string{"pod-template-hash"}, 1048 | }, { 1049 | MaxSkew: 1, 1050 | TopologyKey: "topology.kubernetes.io/zone", 1051 | WhenUnsatisfiable: "ScheduleAnyway", 1052 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test-app"}}, 1053 | NodeAffinityPolicy: &topologyHonorPolicy, 1054 | NodeTaintsPolicy: &topologyHonorPolicy, 1055 | MatchLabelKeys: []string{"pod-template-hash"}, 1056 | }}, 1057 | sourceTopologySpreadConstraints: []corev1.TopologySpreadConstraint{{ 1058 | MaxSkew: 2, 1059 | TopologyKey: "topology.kubernetes.io/zone", 1060 | WhenUnsatisfiable: "ScheduleAnyway", 1061 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test"}}, 1062 | NodeAffinityPolicy: &topologyHonorPolicy, 1063 | NodeTaintsPolicy: &topologyHonorPolicy, 1064 | MatchLabelKeys: []string{"pod-template"}, 1065 | }}, 1066 | path: "/spec/topologySpreadConstraints", 1067 | patch: []patchOperation{{ 1068 | Op: "replace", 1069 | Path: "/spec/topologySpreadConstraints/1", 1070 | Value: corev1.TopologySpreadConstraint{ 1071 | MaxSkew: 2, 1072 | TopologyKey: "topology.kubernetes.io/zone", 1073 | WhenUnsatisfiable: "ScheduleAnyway", 1074 | LabelSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"app.kubernetes.io/name": "test"}}, 1075 | NodeAffinityPolicy: &topologyHonorPolicy, 1076 | NodeTaintsPolicy: &topologyHonorPolicy, 1077 | MatchLabelKeys: []string{"pod-template"}, 1078 | }, 1079 | }}, 1080 | }, 1081 | } 1082 | for _, e := range envs { 1083 | patch := addTopologySpreadConstraints(e.targetTopologySpreadConstraints, e.sourceTopologySpreadConstraints, e.path) 1084 | if !cmp.Equal(patch, e.patch) { 1085 | t.Errorf("addTolerations was incorrect, for %v, got: %v, want: %v.", e.targetTopologySpreadConstraints, patch, e.patch) 1086 | } 1087 | } 1088 | } 1089 | 1090 | func TestUpdateAnnotations(t *testing.T) { 1091 | annos := []struct { 1092 | targetAnno map[string]string 1093 | sourceAnno map[string]string 1094 | patch []patchOperation 1095 | }{ 1096 | {map[string]string{"some-other-annotation": "some_value"}, 1097 | map[string]string{admissionWebhookAnnotationStatusKey: "injected"}, 1098 | []patchOperation{{"add", "/metadata/annotations/" + admissionWebhookAnnotationStatusKey, "injected"}}, 1099 | }, 1100 | {nil, 1101 | map[string]string{admissionWebhookAnnotationStatusKey: "injected"}, 1102 | []patchOperation{{"add", "/metadata/annotations", map[string]string{admissionWebhookAnnotationStatusKey: "injected"}}}, 1103 | }, 1104 | {map[string]string{admissionWebhookAnnotationStatusKey: "some_value"}, 1105 | map[string]string{admissionWebhookAnnotationStatusKey: "injected"}, 1106 | []patchOperation{{"replace", "/metadata/annotations/" + admissionWebhookAnnotationStatusKey, "injected"}}, 1107 | }, 1108 | } 1109 | 1110 | for _, a := range annos { 1111 | patch := updateAnnotation(a.targetAnno, a.sourceAnno) 1112 | if !cmp.Equal(patch, a.patch) { 1113 | t.Errorf("updateAnnotations was incorrect, for %v, got: %v, want: %v.", a.targetAnno, patch, a.patch) 1114 | } 1115 | } 1116 | } 1117 | 1118 | // TestRemovePodAntiAffinity tests the removePodAntiAffinity function. 1119 | func TestRemovePodAntiAffinity(t *testing.T) { 1120 | basePath := "/spec/affinity/podAntiAffinity" 1121 | expectedPatch := []patchOperation{ 1122 | { 1123 | Op: "remove", 1124 | Path: basePath, 1125 | }, 1126 | } 1127 | 1128 | // Call the removePodAntiAffinity function 1129 | patch := removePodAntiAffinity(basePath) 1130 | 1131 | // Check if the returned patch matches the expected patch 1132 | if !cmp.Equal(patch, expectedPatch) { 1133 | t.Errorf("removePodAntiAffinity was incorrect, for got: %v, want: %v.", expectedPatch, patch) 1134 | } 1135 | } 1136 | -------------------------------------------------------------------------------- /k8s-env-injector-build.yaml: -------------------------------------------------------------------------------- 1 | version: v1.0.0 2 | steps: 3 | - build: -t {{.Run.Registry}}/k8s-env-injector:{{.Run.ID}} -f Dockerfile . 4 | - push: ["{{.Run.Registry}}/k8s-env-injector:{{.Run.ID}}"] 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>hmcts/.github:renovate-config" 5 | ] 6 | } 7 | --------------------------------------------------------------------------------