k8s operator for knot hosting
tangled kubernetes

init

Signed-off-by: Josephine Pfeiffer <hi@josie.lol>

+7329
+19
.gitignore
··· 1 + # Binaries 2 + bin/ 3 + *.exe 4 + *.dll 5 + *.so 6 + *.dylib 7 + 8 + # Test artifacts 9 + cover.out 10 + *.test 11 + 12 + # IDE 13 + .idea/ 14 + .vscode/ 15 + *.swp 16 + *.swo 17 + 18 + # OS 19 + .DS_Store
+49
.pre-commit-config.yaml
··· 1 + repos: 2 + - repo: https://github.com/pre-commit/pre-commit-hooks 3 + rev: v5.0.0 4 + hooks: 5 + - id: end-of-file-fixer 6 + exclude: ^config/crd/.*\.yaml$ 7 + stages: [pre-commit] 8 + - id: trailing-whitespace 9 + exclude: ^config/crd/.*\.yaml$ 10 + stages: [pre-commit] 11 + - id: check-added-large-files 12 + stages: [pre-commit] 13 + - id: check-merge-conflict 14 + stages: [pre-commit] 15 + - id: detect-private-key 16 + stages: [pre-commit] 17 + - repo: local 18 + hooks: 19 + - id: go-fmt 20 + name: go fmt 21 + entry: gofmt -l -w 22 + language: system 23 + types: [go] 24 + stages: [pre-commit] 25 + - id: go-vet 26 + name: go vet 27 + entry: go vet ./... 28 + language: system 29 + pass_filenames: false 30 + stages: [pre-commit] 31 + - id: verify-codegen 32 + name: verify generated code 33 + entry: bash -c 'make generate && git diff --exit-code api/ config/crd/ || (echo "Generated code is out of date. Run make generate and commit." && exit 1)' 34 + language: system 35 + pass_filenames: false 36 + stages: [pre-commit] 37 + - id: go-test 38 + name: go test 39 + entry: go test ./... 40 + language: system 41 + pass_filenames: false 42 + stages: [pre-push] 43 + - repo: https://github.com/golangci/golangci-lint 44 + rev: v2.0.2 45 + hooks: 46 + - id: golangci-lint 47 + entry: golangci-lint run 48 + args: [--timeout=5m] 49 + stages: [pre-commit]
+25
.tangled/workflows/ci.yml
··· 1 + when: 2 + - event: ["push"] 3 + branch: ["main", "master"] 4 + - event: ["pull_request"] 5 + 6 + engine: "kubernetes" 7 + image: golang:1.25-alpine 8 + architecture: amd64 9 + 10 + steps: 11 + - name: "Setup" 12 + command: | 13 + apk add --no-cache git gcc musl-dev 14 + go mod download 15 + 16 + - name: "Vet" 17 + command: "go vet ./..." 18 + 19 + - name: "Format check" 20 + command: | 21 + go fmt ./... 22 + git diff --exit-code 23 + 24 + - name: "Test" 25 + command: "go test -v -race -timeout=10m ./..."
+20
Dockerfile
··· 1 + FROM golang:1.25-alpine AS builder 2 + 3 + WORKDIR /workspace 4 + 5 + COPY go.mod go.sum ./ 6 + RUN go mod download 7 + 8 + COPY cmd/ cmd/ 9 + COPY api/ api/ 10 + COPY internal/ internal/ 11 + 12 + RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager cmd/main.go 13 + 14 + FROM gcr.io/distroless/static:nonroot 15 + 16 + WORKDIR / 17 + COPY --from=builder /workspace/manager . 18 + USER 65532:65532 19 + 20 + ENTRYPOINT ["/manager"]
+201
LICENSE
··· 1 + Apache License 2 + Version 2.0, January 2004 3 + http://www.apache.org/licenses/ 4 + 5 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 + 7 + 1. Definitions. 8 + 9 + "License" shall mean the terms and conditions for use, reproduction, 10 + and distribution as defined by Sections 1 through 9 of this document. 11 + 12 + "Licensor" shall mean the copyright owner or entity authorized by 13 + the copyright owner that is granting the License. 14 + 15 + "Legal Entity" shall mean the union of the acting entity and all 16 + other entities that control, are controlled by, or are under common 17 + control with that entity. For the purposes of this definition, 18 + "control" means (i) the power, direct or indirect, to cause the 19 + direction or management of such entity, whether by contract or 20 + otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 + outstanding shares, or (iii) beneficial ownership of such entity. 22 + 23 + "You" (or "Your") shall mean an individual or Legal Entity 24 + exercising permissions granted by this License. 25 + 26 + "Source" form shall mean the preferred form for making modifications, 27 + including but not limited to software source code, documentation 28 + source, and configuration files. 29 + 30 + "Object" form shall mean any form resulting from mechanical 31 + transformation or translation of a Source form, including but 32 + not limited to compiled object code, generated documentation, 33 + and conversions to other media types. 34 + 35 + "Work" shall mean the work of authorship, whether in Source or 36 + Object form, made available under the License, as indicated by a 37 + copyright notice that is included in or attached to the work 38 + (an example is provided in the Appendix below). 39 + 40 + "Derivative Works" shall mean any work, whether in Source or Object 41 + form, that is based on (or derived from) the Work and for which the 42 + editorial revisions, annotations, elaborations, or other modifications 43 + represent, as a whole, an original work of authorship. For the purposes 44 + of this License, Derivative Works shall not include works that remain 45 + separable from, or merely link (or bind by name) to the interfaces of, 46 + the Work and Derivative Works thereof. 47 + 48 + "Contribution" shall mean any work of authorship, including 49 + the original version of the Work and any modifications or additions 50 + to that Work or Derivative Works thereof, that is intentionally 51 + submitted to Licensor for inclusion in the Work by the copyright owner 52 + or by an individual or Legal Entity authorized to submit on behalf of 53 + the copyright owner. For the purposes of this definition, "submitted" 54 + means any form of electronic, verbal, or written communication sent 55 + to the Licensor or its representatives, including but not limited to 56 + communication on electronic mailing lists, source code control systems, 57 + and issue tracking systems that are managed by, or on behalf of, the 58 + Licensor for the purpose of discussing and improving the Work, but 59 + excluding communication that is conspicuously marked or otherwise 60 + designated in writing by the copyright owner as "Not a Contribution." 61 + 62 + "Contributor" shall mean Licensor and any individual or Legal Entity 63 + on behalf of whom a Contribution has been received by Licensor and 64 + subsequently incorporated within the Work. 65 + 66 + 2. Grant of Copyright License. Subject to the terms and conditions of 67 + this License, each Contributor hereby grants to You a perpetual, 68 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 + copyright license to reproduce, prepare Derivative Works of, 70 + publicly display, publicly perform, sublicense, and distribute the 71 + Work and such Derivative Works in Source or Object form. 72 + 73 + 3. Grant of Patent License. Subject to the terms and conditions of 74 + this License, each Contributor hereby grants to You a perpetual, 75 + worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 + (except as stated in this section) patent license to make, have made, 77 + use, offer to sell, sell, import, and otherwise transfer the Work, 78 + where such license applies only to those patent claims licensable 79 + by such Contributor that are necessarily infringed by their 80 + Contribution(s) alone or by combination of their Contribution(s) 81 + with the Work to which such Contribution(s) was submitted. If You 82 + institute patent litigation against any entity (including a 83 + cross-claim or counterclaim in a lawsuit) alleging that the Work 84 + or a Contribution incorporated within the Work constitutes direct 85 + or contributory patent infringement, then any patent licenses 86 + granted to You under this License for that Work shall terminate 87 + as of the date such litigation is filed. 88 + 89 + 4. Redistribution. You may reproduce and distribute copies of the 90 + Work or Derivative Works thereof in any medium, with or without 91 + modifications, and in Source or Object form, provided that You 92 + meet the following conditions: 93 + 94 + (a) You must give any other recipients of the Work or 95 + Derivative Works a copy of this License; and 96 + 97 + (b) You must cause any modified files to carry prominent notices 98 + stating that You changed the files; and 99 + 100 + (c) You must retain, in the Source form of any Derivative Works 101 + that You distribute, all copyright, patent, trademark, and 102 + attribution notices from the Source form of the Work, 103 + excluding those notices that do not pertain to any part of 104 + the Derivative Works; and 105 + 106 + (d) If the Work includes a "NOTICE" text file as part of its 107 + distribution, then any Derivative Works that You distribute must 108 + include a readable copy of the attribution notices contained 109 + within such NOTICE file, excluding those notices that do not 110 + pertain to any part of the Derivative Works, in at least one 111 + of the following places: within a NOTICE text file distributed 112 + as part of the Derivative Works; within the Source form or 113 + documentation, if provided along with the Derivative Works; or, 114 + within a display generated by the Derivative Works, if and 115 + wherever such third-party notices normally appear. The contents 116 + of the NOTICE file are for informational purposes only and 117 + do not modify the License. You may add Your own attribution 118 + notices within Derivative Works that You distribute, alongside 119 + or as an addendum to the NOTICE text from the Work, provided 120 + that such additional attribution notices cannot be construed 121 + as modifying the License. 122 + 123 + You may add Your own copyright statement to Your modifications and 124 + may provide additional or different license terms and conditions 125 + for use, reproduction, or distribution of Your modifications, or 126 + for any such Derivative Works as a whole, provided Your use, 127 + reproduction, and distribution of the Work otherwise complies with 128 + the conditions stated in this License. 129 + 130 + 5. Submission of Contributions. Unless You explicitly state otherwise, 131 + any Contribution intentionally submitted for inclusion in the Work 132 + by You to the Licensor shall be under the terms and conditions of 133 + this License, without any additional terms or conditions. 134 + Notwithstanding the above, nothing herein shall supersede or modify 135 + the terms of any separate license agreement you may have executed 136 + with Licensor regarding such Contributions. 137 + 138 + 6. Trademarks. This License does not grant permission to use the trade 139 + names, trademarks, service marks, or product names of the Licensor, 140 + except as required for reasonable and customary use in describing the 141 + origin of the Work and reproducing the content of the NOTICE file. 142 + 143 + 7. Disclaimer of Warranty. Unless required by applicable law or 144 + agreed to in writing, Licensor provides the Work (and each 145 + Contributor provides its Contributions) on an "AS IS" BASIS, 146 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 + implied, including, without limitation, any warranties or conditions 148 + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 + PARTICULAR PURPOSE. You are solely responsible for determining the 150 + appropriateness of using or redistributing the Work and assume any 151 + risks associated with Your exercise of permissions under this License. 152 + 153 + 8. Limitation of Liability. In no event and under no legal theory, 154 + whether in tort (including negligence), contract, or otherwise, 155 + unless required by applicable law (such as deliberate and grossly 156 + negligent acts) or agreed to in writing, shall any Contributor be 157 + liable to You for damages, including any direct, indirect, special, 158 + incidental, or consequential damages of any character arising as a 159 + result of this License or out of the use or inability to use the 160 + Work (including but not limited to damages for loss of goodwill, 161 + work stoppage, computer failure or malfunction, or any and all 162 + other commercial damages or losses), even if such Contributor 163 + has been advised of the possibility of such damages. 164 + 165 + 9. Accepting Warranty or Additional Liability. While redistributing 166 + the Work or Derivative Works thereof, You may choose to offer, 167 + and charge a fee for, acceptance of support, warranty, indemnity, 168 + or other liability obligations and/or rights consistent with this 169 + License. However, in accepting such obligations, You may act only 170 + on Your own behalf and on Your sole responsibility, not on behalf 171 + of any other Contributor, and only if You agree to indemnify, 172 + defend, and hold each Contributor harmless for any liability 173 + incurred by, or claims asserted against, such Contributor by reason 174 + of your accepting any such warranty or additional liability. 175 + 176 + END OF TERMS AND CONDITIONS 177 + 178 + APPENDIX: How to apply the Apache License to your work. 179 + 180 + To apply the Apache License to your work, attach the following 181 + boilerplate notice, with the fields enclosed by brackets "[]" 182 + replaced with your own identifying information. (Don't include 183 + the brackets!) The text should be enclosed in the appropriate 184 + comment syntax for the file format. We also recommend that a 185 + file or class name and description of purpose be included on the 186 + same "printed page" as the copyright notice for easier 187 + identification within third-party archives. 188 + 189 + Copyright [yyyy] [name of copyright owner] 190 + 191 + Licensed under the Apache License, Version 2.0 (the "License"); 192 + you may not use this file except in compliance with the License. 193 + You may obtain a copy of the License at 194 + 195 + http://www.apache.org/licenses/LICENSE-2.0 196 + 197 + Unless required by applicable law or agreed to in writing, software 198 + distributed under the License is distributed on an "AS IS" BASIS, 199 + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 + See the License for the specific language governing permissions and 201 + limitations under the License.
+44
Makefile
··· 1 + .PHONY: all build test docker-build docker-push install uninstall deploy undeploy manifests generate fmt vet run \ 2 + lint ci ensure-hooks clean tidy deps help unit cover 3 + 4 + IMG ?= knot-operator:latest 5 + NAMESPACE ?= knot-operator-system 6 + VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") 7 + 8 + LDFLAGS = -ldflags "-X main.Version=${VERSION}" 9 + CGO_ENABLED = 0 10 + GTEST_ARGS = -v -race -timeout=10m 11 + 12 + CONTROLLER_GEN ?= $(shell which controller-gen 2>/dev/null || echo "controller-gen") 13 + 14 + all: build 15 + 16 + fmt: 17 + go fmt ./... 18 + 19 + vet: 20 + go vet ./... 21 + 22 + test: fmt vet unit 23 + 24 + unit: 25 + go test $(GTEST_ARGS) ./... 26 + 27 + cover: 28 + go test $(GTEST_ARGS) -coverprofile=cover.out ./... 29 + go tool cover -func=cover.out 30 + 31 + run: fmt vet 32 + go run ./cmd/main.go 33 + 34 + lint: 35 + @echo "Running pre-commit for all files..." 36 + pre-commit run --all-files 37 + @echo "Pre-commit checks passed." 38 + 39 + generate: manifests ## Generate all code and manifests 40 + $(CONTROLLER_GEN) object paths="./..." 41 + 42 + manifests: ## Generate CRD manifests 43 + $(CONTROLLER_GEN) crd paths="./api/..." output:crd:artifacts:config=config/crd 44 + $(CONTROLLER_GEN) rbac:roleName=knot-operator paths="./internal/controller/..." output:rbac:dir=config/rbac
+49
README.md
··· 1 + # knot-operator 2 + 3 + A Kubernetes operator for managing tangled knot resources. 4 + 5 + ## Quickstart 6 + 7 + ### 1. Install the CRD and deploy the operator 8 + 9 + ```bash 10 + make deploy 11 + ``` 12 + 13 + For OpenShift: 14 + 15 + ```bash 16 + make oc-build 17 + make oc-deploy 18 + ``` 19 + 20 + ### 2. Create a Knot resource 21 + 22 + ```yaml 23 + apiVersion: tangled.org/v1alpha1 24 + kind: Knot 25 + metadata: 26 + name: my-knot 27 + spec: 28 + hostname: knot.example.com 29 + owner: did:plc:your-did-here 30 + storage: 31 + repoSize: 20Gi 32 + dbSize: 2Gi 33 + ingress: 34 + enabled: true 35 + ingressClassName: nginx 36 + tls: 37 + enabled: true 38 + secretName: knot-tls 39 + ``` 40 + 41 + ```bash 42 + kubectl apply -f knot.yaml 43 + ``` 44 + 45 + ### 3. Check status 46 + 47 + ```bash 48 + kubectl get knots 49 + ```
+16
api/v1alpha1/groupversion_info.go
··· 1 + // +kubebuilder:object:generate=true 2 + // +groupName=tangled.org 3 + package v1alpha1 4 + 5 + import ( 6 + "k8s.io/apimachinery/pkg/runtime/schema" 7 + "sigs.k8s.io/controller-runtime/pkg/scheme" 8 + ) 9 + 10 + var ( 11 + GroupVersion = schema.GroupVersion{Group: "tangled.org", Version: "v1alpha1"} 12 + 13 + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 14 + 15 + AddToScheme = SchemeBuilder.AddToScheme 16 + )
+294
api/v1alpha1/knot_types.go
··· 1 + package v1alpha1 2 + 3 + import ( 4 + corev1 "k8s.io/api/core/v1" 5 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 + ) 7 + 8 + // KnotSpec defines the desired state of Knot 9 + type KnotSpec struct { 10 + // Image is the container image to use for the Knot server 11 + // +kubebuilder:default="docker.io/tngl/knot:v1.10.0-alpha" 12 + Image string `json:"image,omitempty"` 13 + 14 + // ImagePullPolicy defines the pull policy for the container image 15 + // +kubebuilder:default="IfNotPresent" 16 + ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"` 17 + 18 + // Replicas is the number of Knot server instances to run 19 + // +kubebuilder:default=1 20 + // +kubebuilder:validation:Minimum=1 21 + Replicas int32 `json:"replicas,omitempty"` 22 + 23 + // Hostname is the public hostname for the Knot server (e.g., knot.example.com) 24 + // +kubebuilder:validation:Required 25 + Hostname string `json:"hostname"` 26 + 27 + // Owner is the DID identifier of the server owner 28 + // +kubebuilder:validation:Required 29 + Owner string `json:"owner"` 30 + 31 + // AppviewEndpoint is the appview endpoint URL 32 + // +kubebuilder:default="https://tangled.org" 33 + AppviewEndpoint string `json:"appviewEndpoint,omitempty"` 34 + 35 + // Storage configures persistent storage for repositories and database 36 + Storage KnotStorageSpec `json:"storage,omitempty"` 37 + 38 + // Resources defines compute resource requirements 39 + Resources corev1.ResourceRequirements `json:"resources,omitempty"` 40 + 41 + // ServiceAccountName is the name of the ServiceAccount to use 42 + ServiceAccountName string `json:"serviceAccountName,omitempty"` 43 + 44 + // NodeSelector for pod scheduling 45 + NodeSelector map[string]string `json:"nodeSelector,omitempty"` 46 + 47 + // Tolerations for pod scheduling 48 + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` 49 + 50 + // Affinity rules for pod scheduling 51 + Affinity *corev1.Affinity `json:"affinity,omitempty"` 52 + 53 + // Ingress configures external access (Kubernetes Ingress) 54 + Ingress *KnotIngressSpec `json:"ingress,omitempty"` 55 + 56 + // OpenShift contains OpenShift-specific configuration 57 + OpenShift *KnotOpenShiftSpec `json:"openshift,omitempty"` 58 + 59 + // SSH configures the SSH server for git operations 60 + SSH *KnotSSHSpec `json:"ssh,omitempty"` 61 + 62 + // ExtraEnv allows adding additional environment variables 63 + ExtraEnv []corev1.EnvVar `json:"extraEnv,omitempty"` 64 + } 65 + 66 + // KnotStorageSpec defines storage configuration 67 + type KnotStorageSpec struct { 68 + // RepoStorageClass is the StorageClass for repository storage 69 + RepoStorageClass string `json:"repoStorageClass,omitempty"` 70 + 71 + // RepoSize is the size of the repository PVC 72 + // +kubebuilder:default="10Gi" 73 + RepoSize string `json:"repoSize,omitempty"` 74 + 75 + // DBStorageClass is the StorageClass for database storage 76 + DBStorageClass string `json:"dbStorageClass,omitempty"` 77 + 78 + // DBSize is the size of the database PVC 79 + // +kubebuilder:default="1Gi" 80 + DBSize string `json:"dbSize,omitempty"` 81 + 82 + // RepoPath is the path where repositories are stored 83 + // +kubebuilder:default="/data/repos" 84 + RepoPath string `json:"repoPath,omitempty"` 85 + 86 + // DBPath is the path where the database is stored 87 + // +kubebuilder:default="/data/db" 88 + DBPath string `json:"dbPath,omitempty"` 89 + } 90 + 91 + // KnotIngressSpec defines Kubernetes Ingress configuration 92 + type KnotIngressSpec struct { 93 + // Enabled enables Ingress creation 94 + Enabled bool `json:"enabled,omitempty"` 95 + 96 + // IngressClassName is the IngressClass to use 97 + IngressClassName string `json:"ingressClassName,omitempty"` 98 + 99 + // Annotations to add to the Ingress 100 + Annotations map[string]string `json:"annotations,omitempty"` 101 + 102 + // TLS configures TLS for the Ingress 103 + TLS *KnotIngressTLSSpec `json:"tls,omitempty"` 104 + } 105 + 106 + // KnotIngressTLSSpec defines TLS configuration for Ingress 107 + type KnotIngressTLSSpec struct { 108 + // Enabled enables TLS 109 + Enabled bool `json:"enabled,omitempty"` 110 + 111 + // SecretName is the name of the TLS secret 112 + SecretName string `json:"secretName,omitempty"` 113 + } 114 + 115 + // KnotOpenShiftSpec defines OpenShift-specific configuration 116 + type KnotOpenShiftSpec struct { 117 + // Route configures OpenShift Route creation 118 + Route *KnotRouteSpec `json:"route,omitempty"` 119 + 120 + // SCC configures Security Context Constraints 121 + SCC *KnotSCCSpec `json:"scc,omitempty"` 122 + } 123 + 124 + // KnotRouteSpec defines OpenShift Route configuration 125 + type KnotRouteSpec struct { 126 + // Enabled enables Route creation 127 + Enabled bool `json:"enabled,omitempty"` 128 + 129 + // Annotations to add to the Route 130 + Annotations map[string]string `json:"annotations,omitempty"` 131 + 132 + // TLS configures TLS termination for the Route 133 + TLS *KnotRouteTLSSpec `json:"tls,omitempty"` 134 + 135 + // WildcardPolicy specifies the wildcard policy (None, Subdomain) 136 + // +kubebuilder:default="None" 137 + WildcardPolicy string `json:"wildcardPolicy,omitempty"` 138 + } 139 + 140 + // KnotRouteTLSSpec defines TLS configuration for OpenShift Routes 141 + type KnotRouteTLSSpec struct { 142 + // Termination specifies the TLS termination type (edge, passthrough, reencrypt) 143 + // +kubebuilder:default="edge" 144 + // +kubebuilder:validation:Enum=edge;passthrough;reencrypt 145 + Termination string `json:"termination,omitempty"` 146 + 147 + // InsecureEdgeTerminationPolicy specifies behavior for insecure connections 148 + // +kubebuilder:default="Redirect" 149 + // +kubebuilder:validation:Enum=Allow;Redirect;None 150 + InsecureEdgeTerminationPolicy string `json:"insecureEdgeTerminationPolicy,omitempty"` 151 + 152 + // Certificate is the PEM-encoded certificate 153 + Certificate string `json:"certificate,omitempty"` 154 + 155 + // Key is the PEM-encoded private key 156 + Key string `json:"key,omitempty"` 157 + 158 + // CACertificate is the PEM-encoded CA certificate 159 + CACertificate string `json:"caCertificate,omitempty"` 160 + 161 + // DestinationCACertificate is used for reencrypt termination 162 + DestinationCACertificate string `json:"destinationCACertificate,omitempty"` 163 + } 164 + 165 + // KnotSCCSpec defines Security Context Constraints configuration 166 + type KnotSCCSpec struct { 167 + // Name is the name of the SCC to use or create 168 + // +kubebuilder:default="knot-scc" 169 + Name string `json:"name,omitempty"` 170 + 171 + // Create specifies whether to create a custom SCC 172 + Create bool `json:"create,omitempty"` 173 + 174 + // RunAsUser specifies the run as user strategy 175 + // +kubebuilder:default="MustRunAsNonRoot" 176 + RunAsUser string `json:"runAsUser,omitempty"` 177 + 178 + // SELinuxContext specifies the SELinux context strategy 179 + // +kubebuilder:default="MustRunAs" 180 + SELinuxContext string `json:"seLinuxContext,omitempty"` 181 + 182 + // FSGroup specifies the fs group strategy 183 + // +kubebuilder:default="MustRunAs" 184 + FSGroup string `json:"fsGroup,omitempty"` 185 + 186 + // SupplementalGroups specifies the supplemental groups strategy 187 + // +kubebuilder:default="RunAsAny" 188 + SupplementalGroups string `json:"supplementalGroups,omitempty"` 189 + 190 + // AllowPrivilegedContainer allows privileged containers 191 + // +kubebuilder:default=false 192 + AllowPrivilegedContainer bool `json:"allowPrivilegedContainer,omitempty"` 193 + 194 + // AllowHostNetwork allows host network access 195 + // +kubebuilder:default=false 196 + AllowHostNetwork bool `json:"allowHostNetwork,omitempty"` 197 + 198 + // AllowHostPorts allows host port binding 199 + // +kubebuilder:default=false 200 + AllowHostPorts bool `json:"allowHostPorts,omitempty"` 201 + 202 + // AllowHostPID allows host PID namespace 203 + // +kubebuilder:default=false 204 + AllowHostPID bool `json:"allowHostPID,omitempty"` 205 + 206 + // AllowHostIPC allows host IPC namespace 207 + // +kubebuilder:default=false 208 + AllowHostIPC bool `json:"allowHostIPC,omitempty"` 209 + 210 + // ReadOnlyRootFilesystem requires read-only root filesystem 211 + // +kubebuilder:default=false 212 + ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem,omitempty"` 213 + 214 + // Volumes specifies allowed volume types 215 + Volumes []string `json:"volumes,omitempty"` 216 + } 217 + 218 + // KnotSSHSpec defines SSH server configuration 219 + type KnotSSHSpec struct { 220 + // Enabled enables SSH access for git operations 221 + Enabled bool `json:"enabled,omitempty"` 222 + 223 + // Port is the SSH port to expose 224 + // +kubebuilder:default=22 225 + Port int32 `json:"port,omitempty"` 226 + 227 + // ServiceType is the Kubernetes Service type for SSH 228 + // +kubebuilder:default="LoadBalancer" 229 + ServiceType corev1.ServiceType `json:"serviceType,omitempty"` 230 + 231 + // LoadBalancerIP is the static IP for LoadBalancer type services 232 + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` 233 + 234 + // NodePort is the port to use when ServiceType is NodePort 235 + NodePort int32 `json:"nodePort,omitempty"` 236 + 237 + // Annotations to add to the SSH Service 238 + Annotations map[string]string `json:"annotations,omitempty"` 239 + } 240 + 241 + // KnotStatus defines the observed state of Knot 242 + type KnotStatus struct { 243 + // Phase represents the current phase of the Knot deployment 244 + // +kubebuilder:validation:Enum=Pending;Running;Failed;Unknown 245 + Phase string `json:"phase,omitempty"` 246 + 247 + // Conditions represent the latest available observations 248 + Conditions []metav1.Condition `json:"conditions,omitempty"` 249 + 250 + // ReadyReplicas is the number of ready replicas 251 + ReadyReplicas int32 `json:"readyReplicas,omitempty"` 252 + 253 + // AvailableReplicas is the number of available replicas 254 + AvailableReplicas int32 `json:"availableReplicas,omitempty"` 255 + 256 + // URL is the external URL of the Knot server 257 + URL string `json:"url,omitempty"` 258 + 259 + // SSHURL is the SSH URL for git operations 260 + SSHURL string `json:"sshURL,omitempty"` 261 + 262 + // ObservedGeneration is the most recent generation observed 263 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` 264 + } 265 + 266 + // +kubebuilder:object:root=true 267 + // +kubebuilder:subresource:status 268 + // +kubebuilder:resource:shortName=kt 269 + // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" 270 + // +kubebuilder:printcolumn:name="URL",type="string",JSONPath=".status.url" 271 + // +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas" 272 + // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 273 + 274 + // Knot is the Schema for the knots API 275 + type Knot struct { 276 + metav1.TypeMeta `json:",inline"` 277 + metav1.ObjectMeta `json:"metadata,omitempty"` 278 + 279 + Spec KnotSpec `json:"spec,omitempty"` 280 + Status KnotStatus `json:"status,omitempty"` 281 + } 282 + 283 + // +kubebuilder:object:root=true 284 + 285 + // KnotList contains a list of Knot 286 + type KnotList struct { 287 + metav1.TypeMeta `json:",inline"` 288 + metav1.ListMeta `json:"metadata,omitempty"` 289 + Items []Knot `json:"items"` 290 + } 291 + 292 + func init() { 293 + SchemeBuilder.Register(&Knot{}, &KnotList{}) 294 + }
+316
api/v1alpha1/zz_generated.deepcopy.go
··· 1 + //go:build !ignore_autogenerated 2 + 3 + // Code generated by controller-gen. DO NOT EDIT. 4 + 5 + package v1alpha1 6 + 7 + import ( 8 + "k8s.io/api/core/v1" 9 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 + runtime "k8s.io/apimachinery/pkg/runtime" 11 + ) 12 + 13 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 14 + func (in *Knot) DeepCopyInto(out *Knot) { 15 + *out = *in 16 + out.TypeMeta = in.TypeMeta 17 + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 18 + in.Spec.DeepCopyInto(&out.Spec) 19 + in.Status.DeepCopyInto(&out.Status) 20 + } 21 + 22 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Knot. 23 + func (in *Knot) DeepCopy() *Knot { 24 + if in == nil { 25 + return nil 26 + } 27 + out := new(Knot) 28 + in.DeepCopyInto(out) 29 + return out 30 + } 31 + 32 + // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 33 + func (in *Knot) DeepCopyObject() runtime.Object { 34 + if c := in.DeepCopy(); c != nil { 35 + return c 36 + } 37 + return nil 38 + } 39 + 40 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 41 + func (in *KnotIngressSpec) DeepCopyInto(out *KnotIngressSpec) { 42 + *out = *in 43 + if in.Annotations != nil { 44 + in, out := &in.Annotations, &out.Annotations 45 + *out = make(map[string]string, len(*in)) 46 + for key, val := range *in { 47 + (*out)[key] = val 48 + } 49 + } 50 + if in.TLS != nil { 51 + in, out := &in.TLS, &out.TLS 52 + *out = new(KnotIngressTLSSpec) 53 + **out = **in 54 + } 55 + } 56 + 57 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotIngressSpec. 58 + func (in *KnotIngressSpec) DeepCopy() *KnotIngressSpec { 59 + if in == nil { 60 + return nil 61 + } 62 + out := new(KnotIngressSpec) 63 + in.DeepCopyInto(out) 64 + return out 65 + } 66 + 67 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 68 + func (in *KnotIngressTLSSpec) DeepCopyInto(out *KnotIngressTLSSpec) { 69 + *out = *in 70 + } 71 + 72 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotIngressTLSSpec. 73 + func (in *KnotIngressTLSSpec) DeepCopy() *KnotIngressTLSSpec { 74 + if in == nil { 75 + return nil 76 + } 77 + out := new(KnotIngressTLSSpec) 78 + in.DeepCopyInto(out) 79 + return out 80 + } 81 + 82 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 83 + func (in *KnotList) DeepCopyInto(out *KnotList) { 84 + *out = *in 85 + out.TypeMeta = in.TypeMeta 86 + in.ListMeta.DeepCopyInto(&out.ListMeta) 87 + if in.Items != nil { 88 + in, out := &in.Items, &out.Items 89 + *out = make([]Knot, len(*in)) 90 + for i := range *in { 91 + (*in)[i].DeepCopyInto(&(*out)[i]) 92 + } 93 + } 94 + } 95 + 96 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotList. 97 + func (in *KnotList) DeepCopy() *KnotList { 98 + if in == nil { 99 + return nil 100 + } 101 + out := new(KnotList) 102 + in.DeepCopyInto(out) 103 + return out 104 + } 105 + 106 + // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 107 + func (in *KnotList) DeepCopyObject() runtime.Object { 108 + if c := in.DeepCopy(); c != nil { 109 + return c 110 + } 111 + return nil 112 + } 113 + 114 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 115 + func (in *KnotOpenShiftSpec) DeepCopyInto(out *KnotOpenShiftSpec) { 116 + *out = *in 117 + if in.Route != nil { 118 + in, out := &in.Route, &out.Route 119 + *out = new(KnotRouteSpec) 120 + (*in).DeepCopyInto(*out) 121 + } 122 + if in.SCC != nil { 123 + in, out := &in.SCC, &out.SCC 124 + *out = new(KnotSCCSpec) 125 + (*in).DeepCopyInto(*out) 126 + } 127 + } 128 + 129 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotOpenShiftSpec. 130 + func (in *KnotOpenShiftSpec) DeepCopy() *KnotOpenShiftSpec { 131 + if in == nil { 132 + return nil 133 + } 134 + out := new(KnotOpenShiftSpec) 135 + in.DeepCopyInto(out) 136 + return out 137 + } 138 + 139 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 140 + func (in *KnotRouteSpec) DeepCopyInto(out *KnotRouteSpec) { 141 + *out = *in 142 + if in.Annotations != nil { 143 + in, out := &in.Annotations, &out.Annotations 144 + *out = make(map[string]string, len(*in)) 145 + for key, val := range *in { 146 + (*out)[key] = val 147 + } 148 + } 149 + if in.TLS != nil { 150 + in, out := &in.TLS, &out.TLS 151 + *out = new(KnotRouteTLSSpec) 152 + **out = **in 153 + } 154 + } 155 + 156 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotRouteSpec. 157 + func (in *KnotRouteSpec) DeepCopy() *KnotRouteSpec { 158 + if in == nil { 159 + return nil 160 + } 161 + out := new(KnotRouteSpec) 162 + in.DeepCopyInto(out) 163 + return out 164 + } 165 + 166 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 167 + func (in *KnotRouteTLSSpec) DeepCopyInto(out *KnotRouteTLSSpec) { 168 + *out = *in 169 + } 170 + 171 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotRouteTLSSpec. 172 + func (in *KnotRouteTLSSpec) DeepCopy() *KnotRouteTLSSpec { 173 + if in == nil { 174 + return nil 175 + } 176 + out := new(KnotRouteTLSSpec) 177 + in.DeepCopyInto(out) 178 + return out 179 + } 180 + 181 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 182 + func (in *KnotSCCSpec) DeepCopyInto(out *KnotSCCSpec) { 183 + *out = *in 184 + if in.Volumes != nil { 185 + in, out := &in.Volumes, &out.Volumes 186 + *out = make([]string, len(*in)) 187 + copy(*out, *in) 188 + } 189 + } 190 + 191 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotSCCSpec. 192 + func (in *KnotSCCSpec) DeepCopy() *KnotSCCSpec { 193 + if in == nil { 194 + return nil 195 + } 196 + out := new(KnotSCCSpec) 197 + in.DeepCopyInto(out) 198 + return out 199 + } 200 + 201 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 202 + func (in *KnotSSHSpec) DeepCopyInto(out *KnotSSHSpec) { 203 + *out = *in 204 + if in.Annotations != nil { 205 + in, out := &in.Annotations, &out.Annotations 206 + *out = make(map[string]string, len(*in)) 207 + for key, val := range *in { 208 + (*out)[key] = val 209 + } 210 + } 211 + } 212 + 213 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotSSHSpec. 214 + func (in *KnotSSHSpec) DeepCopy() *KnotSSHSpec { 215 + if in == nil { 216 + return nil 217 + } 218 + out := new(KnotSSHSpec) 219 + in.DeepCopyInto(out) 220 + return out 221 + } 222 + 223 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 224 + func (in *KnotSpec) DeepCopyInto(out *KnotSpec) { 225 + *out = *in 226 + out.Storage = in.Storage 227 + in.Resources.DeepCopyInto(&out.Resources) 228 + if in.NodeSelector != nil { 229 + in, out := &in.NodeSelector, &out.NodeSelector 230 + *out = make(map[string]string, len(*in)) 231 + for key, val := range *in { 232 + (*out)[key] = val 233 + } 234 + } 235 + if in.Tolerations != nil { 236 + in, out := &in.Tolerations, &out.Tolerations 237 + *out = make([]v1.Toleration, len(*in)) 238 + for i := range *in { 239 + (*in)[i].DeepCopyInto(&(*out)[i]) 240 + } 241 + } 242 + if in.Affinity != nil { 243 + in, out := &in.Affinity, &out.Affinity 244 + *out = new(v1.Affinity) 245 + (*in).DeepCopyInto(*out) 246 + } 247 + if in.Ingress != nil { 248 + in, out := &in.Ingress, &out.Ingress 249 + *out = new(KnotIngressSpec) 250 + (*in).DeepCopyInto(*out) 251 + } 252 + if in.OpenShift != nil { 253 + in, out := &in.OpenShift, &out.OpenShift 254 + *out = new(KnotOpenShiftSpec) 255 + (*in).DeepCopyInto(*out) 256 + } 257 + if in.SSH != nil { 258 + in, out := &in.SSH, &out.SSH 259 + *out = new(KnotSSHSpec) 260 + (*in).DeepCopyInto(*out) 261 + } 262 + if in.ExtraEnv != nil { 263 + in, out := &in.ExtraEnv, &out.ExtraEnv 264 + *out = make([]v1.EnvVar, len(*in)) 265 + for i := range *in { 266 + (*in)[i].DeepCopyInto(&(*out)[i]) 267 + } 268 + } 269 + } 270 + 271 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotSpec. 272 + func (in *KnotSpec) DeepCopy() *KnotSpec { 273 + if in == nil { 274 + return nil 275 + } 276 + out := new(KnotSpec) 277 + in.DeepCopyInto(out) 278 + return out 279 + } 280 + 281 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 282 + func (in *KnotStatus) DeepCopyInto(out *KnotStatus) { 283 + *out = *in 284 + if in.Conditions != nil { 285 + in, out := &in.Conditions, &out.Conditions 286 + *out = make([]metav1.Condition, len(*in)) 287 + for i := range *in { 288 + (*in)[i].DeepCopyInto(&(*out)[i]) 289 + } 290 + } 291 + } 292 + 293 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotStatus. 294 + func (in *KnotStatus) DeepCopy() *KnotStatus { 295 + if in == nil { 296 + return nil 297 + } 298 + out := new(KnotStatus) 299 + in.DeepCopyInto(out) 300 + return out 301 + } 302 + 303 + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 304 + func (in *KnotStorageSpec) DeepCopyInto(out *KnotStorageSpec) { 305 + *out = *in 306 + } 307 + 308 + // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KnotStorageSpec. 309 + func (in *KnotStorageSpec) DeepCopy() *KnotStorageSpec { 310 + if in == nil { 311 + return nil 312 + } 313 + out := new(KnotStorageSpec) 314 + in.DeepCopyInto(out) 315 + return out 316 + }
+96
cmd/main.go
··· 1 + package main 2 + 3 + import ( 4 + "flag" 5 + "os" 6 + 7 + "k8s.io/apimachinery/pkg/runtime" 8 + utilruntime "k8s.io/apimachinery/pkg/util/runtime" 9 + clientgoscheme "k8s.io/client-go/kubernetes/scheme" 10 + ctrl "sigs.k8s.io/controller-runtime" 11 + "sigs.k8s.io/controller-runtime/pkg/healthz" 12 + "sigs.k8s.io/controller-runtime/pkg/log/zap" 13 + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 14 + 15 + tangledv1alpha1 "github.com/josie/knot-operator/api/v1alpha1" 16 + "github.com/josie/knot-operator/internal/controller" 17 + ) 18 + 19 + var ( 20 + scheme = runtime.NewScheme() 21 + setupLog = ctrl.Log.WithName("setup") 22 + ) 23 + 24 + func init() { 25 + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 26 + utilruntime.Must(tangledv1alpha1.AddToScheme(scheme)) 27 + } 28 + 29 + func main() { 30 + var metricsAddr string 31 + var enableLeaderElection bool 32 + var probeAddr string 33 + 34 + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 35 + flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 36 + flag.BoolVar(&enableLeaderElection, "leader-elect", false, 37 + "Enable leader election for controller manager. "+ 38 + "Enabling this will ensure there is only one active controller manager.") 39 + 40 + opts := zap.Options{ 41 + Development: true, 42 + } 43 + opts.BindFlags(flag.CommandLine) 44 + flag.Parse() 45 + 46 + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 47 + 48 + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 49 + Scheme: scheme, 50 + Metrics: metricsserver.Options{ 51 + BindAddress: metricsAddr, 52 + }, 53 + HealthProbeBindAddress: probeAddr, 54 + LeaderElection: enableLeaderElection, 55 + LeaderElectionID: "knot-operator.tangled.org", 56 + }) 57 + if err != nil { 58 + setupLog.Error(err, "unable to start manager") 59 + os.Exit(1) 60 + } 61 + 62 + ctx := ctrl.SetupSignalHandler() 63 + 64 + reconciler := &controller.KnotReconciler{ 65 + Client: mgr.GetClient(), 66 + Scheme: mgr.GetScheme(), 67 + } 68 + 69 + reconciler.IsOpenShift = reconciler.DetectOpenShift(ctx) 70 + 71 + if reconciler.IsOpenShift { 72 + setupLog.Info("OpenShift detected, enabling Route and SCC support") 73 + } else { 74 + setupLog.Info("Running on standard Kubernetes") 75 + } 76 + 77 + if err = reconciler.SetupWithManager(mgr); err != nil { 78 + setupLog.Error(err, "unable to create controller", "controller", "Knot") 79 + os.Exit(1) 80 + } 81 + 82 + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 83 + setupLog.Error(err, "unable to set up health check") 84 + os.Exit(1) 85 + } 86 + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 87 + setupLog.Error(err, "unable to set up ready check") 88 + os.Exit(1) 89 + } 90 + 91 + setupLog.Info("starting manager") 92 + if err := mgr.Start(ctx); err != nil { 93 + setupLog.Error(err, "problem running manager") 94 + os.Exit(1) 95 + } 96 + }
+1552
config/crd/_knots.yaml
··· 1 + --- 2 + apiVersion: apiextensions.k8s.io/v1 3 + kind: CustomResourceDefinition 4 + metadata: 5 + annotations: 6 + controller-gen.kubebuilder.io/version: v0.20.0 7 + name: knots. 8 + spec: 9 + group: "" 10 + names: 11 + kind: Knot 12 + listKind: KnotList 13 + plural: knots 14 + shortNames: 15 + - kt 16 + singular: knot 17 + scope: Namespaced 18 + versions: 19 + - additionalPrinterColumns: 20 + - jsonPath: .status.phase 21 + name: Phase 22 + type: string 23 + - jsonPath: .status.url 24 + name: URL 25 + type: string 26 + - jsonPath: .status.readyReplicas 27 + name: Ready 28 + type: integer 29 + - jsonPath: .metadata.creationTimestamp 30 + name: Age 31 + type: date 32 + name: "" 33 + schema: 34 + openAPIV3Schema: 35 + description: Knot is the Schema for the knots API 36 + properties: 37 + apiVersion: 38 + description: |- 39 + APIVersion defines the versioned schema of this representation of an object. 40 + Servers should convert recognized schemas to the latest internal value, and 41 + may reject unrecognized values. 42 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 43 + type: string 44 + kind: 45 + description: |- 46 + Kind is a string value representing the REST resource this object represents. 47 + Servers may infer this from the endpoint the client submits requests to. 48 + Cannot be updated. 49 + In CamelCase. 50 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 51 + type: string 52 + metadata: 53 + type: object 54 + spec: 55 + description: KnotSpec defines the desired state of Knot 56 + properties: 57 + affinity: 58 + description: Affinity rules for pod scheduling 59 + properties: 60 + nodeAffinity: 61 + description: Describes node affinity scheduling rules for the 62 + pod. 63 + properties: 64 + preferredDuringSchedulingIgnoredDuringExecution: 65 + description: |- 66 + The scheduler will prefer to schedule pods to nodes that satisfy 67 + the affinity expressions specified by this field, but it may choose 68 + a node that violates one or more of the expressions. The node that is 69 + most preferred is the one with the greatest sum of weights, i.e. 70 + for each node that meets all of the scheduling requirements (resource 71 + request, requiredDuringScheduling affinity expressions, etc.), 72 + compute a sum by iterating through the elements of this field and adding 73 + "weight" to the sum if the node matches the corresponding matchExpressions; the 74 + node(s) with the highest sum are the most preferred. 75 + items: 76 + description: |- 77 + An empty preferred scheduling term matches all objects with implicit weight 0 78 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). 79 + properties: 80 + preference: 81 + description: A node selector term, associated with the 82 + corresponding weight. 83 + properties: 84 + matchExpressions: 85 + description: A list of node selector requirements 86 + by node's labels. 87 + items: 88 + description: |- 89 + A node selector requirement is a selector that contains values, a key, and an operator 90 + that relates the key and values. 91 + properties: 92 + key: 93 + description: The label key that the selector 94 + applies to. 95 + type: string 96 + operator: 97 + description: |- 98 + Represents a key's relationship to a set of values. 99 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 100 + type: string 101 + values: 102 + description: |- 103 + An array of string values. If the operator is In or NotIn, 104 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 105 + the values array must be empty. If the operator is Gt or Lt, the values 106 + array must have a single element, which will be interpreted as an integer. 107 + This array is replaced during a strategic merge patch. 108 + items: 109 + type: string 110 + type: array 111 + x-kubernetes-list-type: atomic 112 + required: 113 + - key 114 + - operator 115 + type: object 116 + type: array 117 + x-kubernetes-list-type: atomic 118 + matchFields: 119 + description: A list of node selector requirements 120 + by node's fields. 121 + items: 122 + description: |- 123 + A node selector requirement is a selector that contains values, a key, and an operator 124 + that relates the key and values. 125 + properties: 126 + key: 127 + description: The label key that the selector 128 + applies to. 129 + type: string 130 + operator: 131 + description: |- 132 + Represents a key's relationship to a set of values. 133 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 134 + type: string 135 + values: 136 + description: |- 137 + An array of string values. If the operator is In or NotIn, 138 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 139 + the values array must be empty. If the operator is Gt or Lt, the values 140 + array must have a single element, which will be interpreted as an integer. 141 + This array is replaced during a strategic merge patch. 142 + items: 143 + type: string 144 + type: array 145 + x-kubernetes-list-type: atomic 146 + required: 147 + - key 148 + - operator 149 + type: object 150 + type: array 151 + x-kubernetes-list-type: atomic 152 + type: object 153 + x-kubernetes-map-type: atomic 154 + weight: 155 + description: Weight associated with matching the corresponding 156 + nodeSelectorTerm, in the range 1-100. 157 + format: int32 158 + type: integer 159 + required: 160 + - preference 161 + - weight 162 + type: object 163 + type: array 164 + x-kubernetes-list-type: atomic 165 + requiredDuringSchedulingIgnoredDuringExecution: 166 + description: |- 167 + If the affinity requirements specified by this field are not met at 168 + scheduling time, the pod will not be scheduled onto the node. 169 + If the affinity requirements specified by this field cease to be met 170 + at some point during pod execution (e.g. due to an update), the system 171 + may or may not try to eventually evict the pod from its node. 172 + properties: 173 + nodeSelectorTerms: 174 + description: Required. A list of node selector terms. 175 + The terms are ORed. 176 + items: 177 + description: |- 178 + A null or empty node selector term matches no objects. The requirements of 179 + them are ANDed. 180 + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. 181 + properties: 182 + matchExpressions: 183 + description: A list of node selector requirements 184 + by node's labels. 185 + items: 186 + description: |- 187 + A node selector requirement is a selector that contains values, a key, and an operator 188 + that relates the key and values. 189 + properties: 190 + key: 191 + description: The label key that the selector 192 + applies to. 193 + type: string 194 + operator: 195 + description: |- 196 + Represents a key's relationship to a set of values. 197 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 198 + type: string 199 + values: 200 + description: |- 201 + An array of string values. If the operator is In or NotIn, 202 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 203 + the values array must be empty. If the operator is Gt or Lt, the values 204 + array must have a single element, which will be interpreted as an integer. 205 + This array is replaced during a strategic merge patch. 206 + items: 207 + type: string 208 + type: array 209 + x-kubernetes-list-type: atomic 210 + required: 211 + - key 212 + - operator 213 + type: object 214 + type: array 215 + x-kubernetes-list-type: atomic 216 + matchFields: 217 + description: A list of node selector requirements 218 + by node's fields. 219 + items: 220 + description: |- 221 + A node selector requirement is a selector that contains values, a key, and an operator 222 + that relates the key and values. 223 + properties: 224 + key: 225 + description: The label key that the selector 226 + applies to. 227 + type: string 228 + operator: 229 + description: |- 230 + Represents a key's relationship to a set of values. 231 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 232 + type: string 233 + values: 234 + description: |- 235 + An array of string values. If the operator is In or NotIn, 236 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 237 + the values array must be empty. If the operator is Gt or Lt, the values 238 + array must have a single element, which will be interpreted as an integer. 239 + This array is replaced during a strategic merge patch. 240 + items: 241 + type: string 242 + type: array 243 + x-kubernetes-list-type: atomic 244 + required: 245 + - key 246 + - operator 247 + type: object 248 + type: array 249 + x-kubernetes-list-type: atomic 250 + type: object 251 + x-kubernetes-map-type: atomic 252 + type: array 253 + x-kubernetes-list-type: atomic 254 + required: 255 + - nodeSelectorTerms 256 + type: object 257 + x-kubernetes-map-type: atomic 258 + type: object 259 + podAffinity: 260 + description: Describes pod affinity scheduling rules (e.g. co-locate 261 + this pod in the same node, zone, etc. as some other pod(s)). 262 + properties: 263 + preferredDuringSchedulingIgnoredDuringExecution: 264 + description: |- 265 + The scheduler will prefer to schedule pods to nodes that satisfy 266 + the affinity expressions specified by this field, but it may choose 267 + a node that violates one or more of the expressions. The node that is 268 + most preferred is the one with the greatest sum of weights, i.e. 269 + for each node that meets all of the scheduling requirements (resource 270 + request, requiredDuringScheduling affinity expressions, etc.), 271 + compute a sum by iterating through the elements of this field and adding 272 + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the 273 + node(s) with the highest sum are the most preferred. 274 + items: 275 + description: The weights of all of the matched WeightedPodAffinityTerm 276 + fields are added per-node to find the most preferred node(s) 277 + properties: 278 + podAffinityTerm: 279 + description: Required. A pod affinity term, associated 280 + with the corresponding weight. 281 + properties: 282 + labelSelector: 283 + description: |- 284 + A label query over a set of resources, in this case pods. 285 + If it's null, this PodAffinityTerm matches with no Pods. 286 + properties: 287 + matchExpressions: 288 + description: matchExpressions is a list of label 289 + selector requirements. The requirements are 290 + ANDed. 291 + items: 292 + description: |- 293 + A label selector requirement is a selector that contains values, a key, and an operator that 294 + relates the key and values. 295 + properties: 296 + key: 297 + description: key is the label key that 298 + the selector applies to. 299 + type: string 300 + operator: 301 + description: |- 302 + operator represents a key's relationship to a set of values. 303 + Valid operators are In, NotIn, Exists and DoesNotExist. 304 + type: string 305 + values: 306 + description: |- 307 + values is an array of string values. If the operator is In or NotIn, 308 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 309 + the values array must be empty. This array is replaced during a strategic 310 + merge patch. 311 + items: 312 + type: string 313 + type: array 314 + x-kubernetes-list-type: atomic 315 + required: 316 + - key 317 + - operator 318 + type: object 319 + type: array 320 + x-kubernetes-list-type: atomic 321 + matchLabels: 322 + additionalProperties: 323 + type: string 324 + description: |- 325 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 326 + map is equivalent to an element of matchExpressions, whose key field is "key", the 327 + operator is "In", and the values array contains only "value". The requirements are ANDed. 328 + type: object 329 + type: object 330 + x-kubernetes-map-type: atomic 331 + matchLabelKeys: 332 + description: |- 333 + MatchLabelKeys is a set of pod label keys to select which pods will 334 + be taken into consideration. The keys are used to lookup values from the 335 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 336 + to select the group of existing pods which pods will be taken into consideration 337 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 338 + pod labels will be ignored. The default value is empty. 339 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 340 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 341 + items: 342 + type: string 343 + type: array 344 + x-kubernetes-list-type: atomic 345 + mismatchLabelKeys: 346 + description: |- 347 + MismatchLabelKeys is a set of pod label keys to select which pods will 348 + be taken into consideration. The keys are used to lookup values from the 349 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 350 + to select the group of existing pods which pods will be taken into consideration 351 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 352 + pod labels will be ignored. The default value is empty. 353 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 354 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 355 + items: 356 + type: string 357 + type: array 358 + x-kubernetes-list-type: atomic 359 + namespaceSelector: 360 + description: |- 361 + A label query over the set of namespaces that the term applies to. 362 + The term is applied to the union of the namespaces selected by this field 363 + and the ones listed in the namespaces field. 364 + null selector and null or empty namespaces list means "this pod's namespace". 365 + An empty selector ({}) matches all namespaces. 366 + properties: 367 + matchExpressions: 368 + description: matchExpressions is a list of label 369 + selector requirements. The requirements are 370 + ANDed. 371 + items: 372 + description: |- 373 + A label selector requirement is a selector that contains values, a key, and an operator that 374 + relates the key and values. 375 + properties: 376 + key: 377 + description: key is the label key that 378 + the selector applies to. 379 + type: string 380 + operator: 381 + description: |- 382 + operator represents a key's relationship to a set of values. 383 + Valid operators are In, NotIn, Exists and DoesNotExist. 384 + type: string 385 + values: 386 + description: |- 387 + values is an array of string values. If the operator is In or NotIn, 388 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 389 + the values array must be empty. This array is replaced during a strategic 390 + merge patch. 391 + items: 392 + type: string 393 + type: array 394 + x-kubernetes-list-type: atomic 395 + required: 396 + - key 397 + - operator 398 + type: object 399 + type: array 400 + x-kubernetes-list-type: atomic 401 + matchLabels: 402 + additionalProperties: 403 + type: string 404 + description: |- 405 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 406 + map is equivalent to an element of matchExpressions, whose key field is "key", the 407 + operator is "In", and the values array contains only "value". The requirements are ANDed. 408 + type: object 409 + type: object 410 + x-kubernetes-map-type: atomic 411 + namespaces: 412 + description: |- 413 + namespaces specifies a static list of namespace names that the term applies to. 414 + The term is applied to the union of the namespaces listed in this field 415 + and the ones selected by namespaceSelector. 416 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 417 + items: 418 + type: string 419 + type: array 420 + x-kubernetes-list-type: atomic 421 + topologyKey: 422 + description: |- 423 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 424 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 425 + whose value of the label with key topologyKey matches that of any node on which any of the 426 + selected pods is running. 427 + Empty topologyKey is not allowed. 428 + type: string 429 + required: 430 + - topologyKey 431 + type: object 432 + weight: 433 + description: |- 434 + weight associated with matching the corresponding podAffinityTerm, 435 + in the range 1-100. 436 + format: int32 437 + type: integer 438 + required: 439 + - podAffinityTerm 440 + - weight 441 + type: object 442 + type: array 443 + x-kubernetes-list-type: atomic 444 + requiredDuringSchedulingIgnoredDuringExecution: 445 + description: |- 446 + If the affinity requirements specified by this field are not met at 447 + scheduling time, the pod will not be scheduled onto the node. 448 + If the affinity requirements specified by this field cease to be met 449 + at some point during pod execution (e.g. due to a pod label update), the 450 + system may or may not try to eventually evict the pod from its node. 451 + When there are multiple elements, the lists of nodes corresponding to each 452 + podAffinityTerm are intersected, i.e. all terms must be satisfied. 453 + items: 454 + description: |- 455 + Defines a set of pods (namely those matching the labelSelector 456 + relative to the given namespace(s)) that this pod should be 457 + co-located (affinity) or not co-located (anti-affinity) with, 458 + where co-located is defined as running on a node whose value of 459 + the label with key <topologyKey> matches that of any node on which 460 + a pod of the set of pods is running 461 + properties: 462 + labelSelector: 463 + description: |- 464 + A label query over a set of resources, in this case pods. 465 + If it's null, this PodAffinityTerm matches with no Pods. 466 + properties: 467 + matchExpressions: 468 + description: matchExpressions is a list of label 469 + selector requirements. The requirements are ANDed. 470 + items: 471 + description: |- 472 + A label selector requirement is a selector that contains values, a key, and an operator that 473 + relates the key and values. 474 + properties: 475 + key: 476 + description: key is the label key that the 477 + selector applies to. 478 + type: string 479 + operator: 480 + description: |- 481 + operator represents a key's relationship to a set of values. 482 + Valid operators are In, NotIn, Exists and DoesNotExist. 483 + type: string 484 + values: 485 + description: |- 486 + values is an array of string values. If the operator is In or NotIn, 487 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 488 + the values array must be empty. This array is replaced during a strategic 489 + merge patch. 490 + items: 491 + type: string 492 + type: array 493 + x-kubernetes-list-type: atomic 494 + required: 495 + - key 496 + - operator 497 + type: object 498 + type: array 499 + x-kubernetes-list-type: atomic 500 + matchLabels: 501 + additionalProperties: 502 + type: string 503 + description: |- 504 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 505 + map is equivalent to an element of matchExpressions, whose key field is "key", the 506 + operator is "In", and the values array contains only "value". The requirements are ANDed. 507 + type: object 508 + type: object 509 + x-kubernetes-map-type: atomic 510 + matchLabelKeys: 511 + description: |- 512 + MatchLabelKeys is a set of pod label keys to select which pods will 513 + be taken into consideration. The keys are used to lookup values from the 514 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 515 + to select the group of existing pods which pods will be taken into consideration 516 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 517 + pod labels will be ignored. The default value is empty. 518 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 519 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 520 + items: 521 + type: string 522 + type: array 523 + x-kubernetes-list-type: atomic 524 + mismatchLabelKeys: 525 + description: |- 526 + MismatchLabelKeys is a set of pod label keys to select which pods will 527 + be taken into consideration. The keys are used to lookup values from the 528 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 529 + to select the group of existing pods which pods will be taken into consideration 530 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 531 + pod labels will be ignored. The default value is empty. 532 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 533 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 534 + items: 535 + type: string 536 + type: array 537 + x-kubernetes-list-type: atomic 538 + namespaceSelector: 539 + description: |- 540 + A label query over the set of namespaces that the term applies to. 541 + The term is applied to the union of the namespaces selected by this field 542 + and the ones listed in the namespaces field. 543 + null selector and null or empty namespaces list means "this pod's namespace". 544 + An empty selector ({}) matches all namespaces. 545 + properties: 546 + matchExpressions: 547 + description: matchExpressions is a list of label 548 + selector requirements. The requirements are ANDed. 549 + items: 550 + description: |- 551 + A label selector requirement is a selector that contains values, a key, and an operator that 552 + relates the key and values. 553 + properties: 554 + key: 555 + description: key is the label key that the 556 + selector applies to. 557 + type: string 558 + operator: 559 + description: |- 560 + operator represents a key's relationship to a set of values. 561 + Valid operators are In, NotIn, Exists and DoesNotExist. 562 + type: string 563 + values: 564 + description: |- 565 + values is an array of string values. If the operator is In or NotIn, 566 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 567 + the values array must be empty. This array is replaced during a strategic 568 + merge patch. 569 + items: 570 + type: string 571 + type: array 572 + x-kubernetes-list-type: atomic 573 + required: 574 + - key 575 + - operator 576 + type: object 577 + type: array 578 + x-kubernetes-list-type: atomic 579 + matchLabels: 580 + additionalProperties: 581 + type: string 582 + description: |- 583 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 584 + map is equivalent to an element of matchExpressions, whose key field is "key", the 585 + operator is "In", and the values array contains only "value". The requirements are ANDed. 586 + type: object 587 + type: object 588 + x-kubernetes-map-type: atomic 589 + namespaces: 590 + description: |- 591 + namespaces specifies a static list of namespace names that the term applies to. 592 + The term is applied to the union of the namespaces listed in this field 593 + and the ones selected by namespaceSelector. 594 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 595 + items: 596 + type: string 597 + type: array 598 + x-kubernetes-list-type: atomic 599 + topologyKey: 600 + description: |- 601 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 602 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 603 + whose value of the label with key topologyKey matches that of any node on which any of the 604 + selected pods is running. 605 + Empty topologyKey is not allowed. 606 + type: string 607 + required: 608 + - topologyKey 609 + type: object 610 + type: array 611 + x-kubernetes-list-type: atomic 612 + type: object 613 + podAntiAffinity: 614 + description: Describes pod anti-affinity scheduling rules (e.g. 615 + avoid putting this pod in the same node, zone, etc. as some 616 + other pod(s)). 617 + properties: 618 + preferredDuringSchedulingIgnoredDuringExecution: 619 + description: |- 620 + The scheduler will prefer to schedule pods to nodes that satisfy 621 + the anti-affinity expressions specified by this field, but it may choose 622 + a node that violates one or more of the expressions. The node that is 623 + most preferred is the one with the greatest sum of weights, i.e. 624 + for each node that meets all of the scheduling requirements (resource 625 + request, requiredDuringScheduling anti-affinity expressions, etc.), 626 + compute a sum by iterating through the elements of this field and subtracting 627 + "weight" from the sum if the node has pods which matches the corresponding podAffinityTerm; the 628 + node(s) with the highest sum are the most preferred. 629 + items: 630 + description: The weights of all of the matched WeightedPodAffinityTerm 631 + fields are added per-node to find the most preferred node(s) 632 + properties: 633 + podAffinityTerm: 634 + description: Required. A pod affinity term, associated 635 + with the corresponding weight. 636 + properties: 637 + labelSelector: 638 + description: |- 639 + A label query over a set of resources, in this case pods. 640 + If it's null, this PodAffinityTerm matches with no Pods. 641 + properties: 642 + matchExpressions: 643 + description: matchExpressions is a list of label 644 + selector requirements. The requirements are 645 + ANDed. 646 + items: 647 + description: |- 648 + A label selector requirement is a selector that contains values, a key, and an operator that 649 + relates the key and values. 650 + properties: 651 + key: 652 + description: key is the label key that 653 + the selector applies to. 654 + type: string 655 + operator: 656 + description: |- 657 + operator represents a key's relationship to a set of values. 658 + Valid operators are In, NotIn, Exists and DoesNotExist. 659 + type: string 660 + values: 661 + description: |- 662 + values is an array of string values. If the operator is In or NotIn, 663 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 664 + the values array must be empty. This array is replaced during a strategic 665 + merge patch. 666 + items: 667 + type: string 668 + type: array 669 + x-kubernetes-list-type: atomic 670 + required: 671 + - key 672 + - operator 673 + type: object 674 + type: array 675 + x-kubernetes-list-type: atomic 676 + matchLabels: 677 + additionalProperties: 678 + type: string 679 + description: |- 680 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 681 + map is equivalent to an element of matchExpressions, whose key field is "key", the 682 + operator is "In", and the values array contains only "value". The requirements are ANDed. 683 + type: object 684 + type: object 685 + x-kubernetes-map-type: atomic 686 + matchLabelKeys: 687 + description: |- 688 + MatchLabelKeys is a set of pod label keys to select which pods will 689 + be taken into consideration. The keys are used to lookup values from the 690 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 691 + to select the group of existing pods which pods will be taken into consideration 692 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 693 + pod labels will be ignored. The default value is empty. 694 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 695 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 696 + items: 697 + type: string 698 + type: array 699 + x-kubernetes-list-type: atomic 700 + mismatchLabelKeys: 701 + description: |- 702 + MismatchLabelKeys is a set of pod label keys to select which pods will 703 + be taken into consideration. The keys are used to lookup values from the 704 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 705 + to select the group of existing pods which pods will be taken into consideration 706 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 707 + pod labels will be ignored. The default value is empty. 708 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 709 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 710 + items: 711 + type: string 712 + type: array 713 + x-kubernetes-list-type: atomic 714 + namespaceSelector: 715 + description: |- 716 + A label query over the set of namespaces that the term applies to. 717 + The term is applied to the union of the namespaces selected by this field 718 + and the ones listed in the namespaces field. 719 + null selector and null or empty namespaces list means "this pod's namespace". 720 + An empty selector ({}) matches all namespaces. 721 + properties: 722 + matchExpressions: 723 + description: matchExpressions is a list of label 724 + selector requirements. The requirements are 725 + ANDed. 726 + items: 727 + description: |- 728 + A label selector requirement is a selector that contains values, a key, and an operator that 729 + relates the key and values. 730 + properties: 731 + key: 732 + description: key is the label key that 733 + the selector applies to. 734 + type: string 735 + operator: 736 + description: |- 737 + operator represents a key's relationship to a set of values. 738 + Valid operators are In, NotIn, Exists and DoesNotExist. 739 + type: string 740 + values: 741 + description: |- 742 + values is an array of string values. If the operator is In or NotIn, 743 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 744 + the values array must be empty. This array is replaced during a strategic 745 + merge patch. 746 + items: 747 + type: string 748 + type: array 749 + x-kubernetes-list-type: atomic 750 + required: 751 + - key 752 + - operator 753 + type: object 754 + type: array 755 + x-kubernetes-list-type: atomic 756 + matchLabels: 757 + additionalProperties: 758 + type: string 759 + description: |- 760 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 761 + map is equivalent to an element of matchExpressions, whose key field is "key", the 762 + operator is "In", and the values array contains only "value". The requirements are ANDed. 763 + type: object 764 + type: object 765 + x-kubernetes-map-type: atomic 766 + namespaces: 767 + description: |- 768 + namespaces specifies a static list of namespace names that the term applies to. 769 + The term is applied to the union of the namespaces listed in this field 770 + and the ones selected by namespaceSelector. 771 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 772 + items: 773 + type: string 774 + type: array 775 + x-kubernetes-list-type: atomic 776 + topologyKey: 777 + description: |- 778 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 779 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 780 + whose value of the label with key topologyKey matches that of any node on which any of the 781 + selected pods is running. 782 + Empty topologyKey is not allowed. 783 + type: string 784 + required: 785 + - topologyKey 786 + type: object 787 + weight: 788 + description: |- 789 + weight associated with matching the corresponding podAffinityTerm, 790 + in the range 1-100. 791 + format: int32 792 + type: integer 793 + required: 794 + - podAffinityTerm 795 + - weight 796 + type: object 797 + type: array 798 + x-kubernetes-list-type: atomic 799 + requiredDuringSchedulingIgnoredDuringExecution: 800 + description: |- 801 + If the anti-affinity requirements specified by this field are not met at 802 + scheduling time, the pod will not be scheduled onto the node. 803 + If the anti-affinity requirements specified by this field cease to be met 804 + at some point during pod execution (e.g. due to a pod label update), the 805 + system may or may not try to eventually evict the pod from its node. 806 + When there are multiple elements, the lists of nodes corresponding to each 807 + podAffinityTerm are intersected, i.e. all terms must be satisfied. 808 + items: 809 + description: |- 810 + Defines a set of pods (namely those matching the labelSelector 811 + relative to the given namespace(s)) that this pod should be 812 + co-located (affinity) or not co-located (anti-affinity) with, 813 + where co-located is defined as running on a node whose value of 814 + the label with key <topologyKey> matches that of any node on which 815 + a pod of the set of pods is running 816 + properties: 817 + labelSelector: 818 + description: |- 819 + A label query over a set of resources, in this case pods. 820 + If it's null, this PodAffinityTerm matches with no Pods. 821 + properties: 822 + matchExpressions: 823 + description: matchExpressions is a list of label 824 + selector requirements. The requirements are ANDed. 825 + items: 826 + description: |- 827 + A label selector requirement is a selector that contains values, a key, and an operator that 828 + relates the key and values. 829 + properties: 830 + key: 831 + description: key is the label key that the 832 + selector applies to. 833 + type: string 834 + operator: 835 + description: |- 836 + operator represents a key's relationship to a set of values. 837 + Valid operators are In, NotIn, Exists and DoesNotExist. 838 + type: string 839 + values: 840 + description: |- 841 + values is an array of string values. If the operator is In or NotIn, 842 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 843 + the values array must be empty. This array is replaced during a strategic 844 + merge patch. 845 + items: 846 + type: string 847 + type: array 848 + x-kubernetes-list-type: atomic 849 + required: 850 + - key 851 + - operator 852 + type: object 853 + type: array 854 + x-kubernetes-list-type: atomic 855 + matchLabels: 856 + additionalProperties: 857 + type: string 858 + description: |- 859 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 860 + map is equivalent to an element of matchExpressions, whose key field is "key", the 861 + operator is "In", and the values array contains only "value". The requirements are ANDed. 862 + type: object 863 + type: object 864 + x-kubernetes-map-type: atomic 865 + matchLabelKeys: 866 + description: |- 867 + MatchLabelKeys is a set of pod label keys to select which pods will 868 + be taken into consideration. The keys are used to lookup values from the 869 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 870 + to select the group of existing pods which pods will be taken into consideration 871 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 872 + pod labels will be ignored. The default value is empty. 873 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 874 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 875 + items: 876 + type: string 877 + type: array 878 + x-kubernetes-list-type: atomic 879 + mismatchLabelKeys: 880 + description: |- 881 + MismatchLabelKeys is a set of pod label keys to select which pods will 882 + be taken into consideration. The keys are used to lookup values from the 883 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 884 + to select the group of existing pods which pods will be taken into consideration 885 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 886 + pod labels will be ignored. The default value is empty. 887 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 888 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 889 + items: 890 + type: string 891 + type: array 892 + x-kubernetes-list-type: atomic 893 + namespaceSelector: 894 + description: |- 895 + A label query over the set of namespaces that the term applies to. 896 + The term is applied to the union of the namespaces selected by this field 897 + and the ones listed in the namespaces field. 898 + null selector and null or empty namespaces list means "this pod's namespace". 899 + An empty selector ({}) matches all namespaces. 900 + properties: 901 + matchExpressions: 902 + description: matchExpressions is a list of label 903 + selector requirements. The requirements are ANDed. 904 + items: 905 + description: |- 906 + A label selector requirement is a selector that contains values, a key, and an operator that 907 + relates the key and values. 908 + properties: 909 + key: 910 + description: key is the label key that the 911 + selector applies to. 912 + type: string 913 + operator: 914 + description: |- 915 + operator represents a key's relationship to a set of values. 916 + Valid operators are In, NotIn, Exists and DoesNotExist. 917 + type: string 918 + values: 919 + description: |- 920 + values is an array of string values. If the operator is In or NotIn, 921 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 922 + the values array must be empty. This array is replaced during a strategic 923 + merge patch. 924 + items: 925 + type: string 926 + type: array 927 + x-kubernetes-list-type: atomic 928 + required: 929 + - key 930 + - operator 931 + type: object 932 + type: array 933 + x-kubernetes-list-type: atomic 934 + matchLabels: 935 + additionalProperties: 936 + type: string 937 + description: |- 938 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 939 + map is equivalent to an element of matchExpressions, whose key field is "key", the 940 + operator is "In", and the values array contains only "value". The requirements are ANDed. 941 + type: object 942 + type: object 943 + x-kubernetes-map-type: atomic 944 + namespaces: 945 + description: |- 946 + namespaces specifies a static list of namespace names that the term applies to. 947 + The term is applied to the union of the namespaces listed in this field 948 + and the ones selected by namespaceSelector. 949 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 950 + items: 951 + type: string 952 + type: array 953 + x-kubernetes-list-type: atomic 954 + topologyKey: 955 + description: |- 956 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 957 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 958 + whose value of the label with key topologyKey matches that of any node on which any of the 959 + selected pods is running. 960 + Empty topologyKey is not allowed. 961 + type: string 962 + required: 963 + - topologyKey 964 + type: object 965 + type: array 966 + x-kubernetes-list-type: atomic 967 + type: object 968 + type: object 969 + appviewEndpoint: 970 + default: https://tangled.org 971 + description: AppviewEndpoint is the appview endpoint URL 972 + type: string 973 + extraEnv: 974 + description: ExtraEnv allows adding additional environment variables 975 + items: 976 + description: EnvVar represents an environment variable present in 977 + a Container. 978 + properties: 979 + name: 980 + description: |- 981 + Name of the environment variable. 982 + May consist of any printable ASCII characters except '='. 983 + type: string 984 + value: 985 + description: |- 986 + Variable references $(VAR_NAME) are expanded 987 + using the previously defined environment variables in the container and 988 + any service environment variables. If a variable cannot be resolved, 989 + the reference in the input string will be unchanged. Double $$ are reduced 990 + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. 991 + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". 992 + Escaped references will never be expanded, regardless of whether the variable 993 + exists or not. 994 + Defaults to "". 995 + type: string 996 + valueFrom: 997 + description: Source for the environment variable's value. Cannot 998 + be used if value is not empty. 999 + properties: 1000 + configMapKeyRef: 1001 + description: Selects a key of a ConfigMap. 1002 + properties: 1003 + key: 1004 + description: The key to select. 1005 + type: string 1006 + name: 1007 + default: "" 1008 + description: |- 1009 + Name of the referent. 1010 + This field is effectively required, but due to backwards compatibility is 1011 + allowed to be empty. Instances of this type with an empty value here are 1012 + almost certainly wrong. 1013 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 1014 + type: string 1015 + optional: 1016 + description: Specify whether the ConfigMap or its key 1017 + must be defined 1018 + type: boolean 1019 + required: 1020 + - key 1021 + type: object 1022 + x-kubernetes-map-type: atomic 1023 + fieldRef: 1024 + description: |- 1025 + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, 1026 + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. 1027 + properties: 1028 + apiVersion: 1029 + description: Version of the schema the FieldPath is 1030 + written in terms of, defaults to "v1". 1031 + type: string 1032 + fieldPath: 1033 + description: Path of the field to select in the specified 1034 + API version. 1035 + type: string 1036 + required: 1037 + - fieldPath 1038 + type: object 1039 + x-kubernetes-map-type: atomic 1040 + fileKeyRef: 1041 + description: |- 1042 + FileKeyRef selects a key of the env file. 1043 + Requires the EnvFiles feature gate to be enabled. 1044 + properties: 1045 + key: 1046 + description: |- 1047 + The key within the env file. An invalid key will prevent the pod from starting. 1048 + The keys defined within a source may consist of any printable ASCII characters except '='. 1049 + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. 1050 + type: string 1051 + optional: 1052 + default: false 1053 + description: |- 1054 + Specify whether the file or its key must be defined. If the file or key 1055 + does not exist, then the env var is not published. 1056 + If optional is set to true and the specified key does not exist, 1057 + the environment variable will not be set in the Pod's containers. 1058 + 1059 + If optional is set to false and the specified key does not exist, 1060 + an error will be returned during Pod creation. 1061 + type: boolean 1062 + path: 1063 + description: |- 1064 + The path within the volume from which to select the file. 1065 + Must be relative and may not contain the '..' path or start with '..'. 1066 + type: string 1067 + volumeName: 1068 + description: The name of the volume mount containing 1069 + the env file. 1070 + type: string 1071 + required: 1072 + - key 1073 + - path 1074 + - volumeName 1075 + type: object 1076 + x-kubernetes-map-type: atomic 1077 + resourceFieldRef: 1078 + description: |- 1079 + Selects a resource of the container: only resources limits and requests 1080 + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. 1081 + properties: 1082 + containerName: 1083 + description: 'Container name: required for volumes, 1084 + optional for env vars' 1085 + type: string 1086 + divisor: 1087 + anyOf: 1088 + - type: integer 1089 + - type: string 1090 + description: Specifies the output format of the exposed 1091 + resources, defaults to "1" 1092 + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1093 + x-kubernetes-int-or-string: true 1094 + resource: 1095 + description: 'Required: resource to select' 1096 + type: string 1097 + required: 1098 + - resource 1099 + type: object 1100 + x-kubernetes-map-type: atomic 1101 + secretKeyRef: 1102 + description: Selects a key of a secret in the pod's namespace 1103 + properties: 1104 + key: 1105 + description: The key of the secret to select from. Must 1106 + be a valid secret key. 1107 + type: string 1108 + name: 1109 + default: "" 1110 + description: |- 1111 + Name of the referent. 1112 + This field is effectively required, but due to backwards compatibility is 1113 + allowed to be empty. Instances of this type with an empty value here are 1114 + almost certainly wrong. 1115 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 1116 + type: string 1117 + optional: 1118 + description: Specify whether the Secret or its key must 1119 + be defined 1120 + type: boolean 1121 + required: 1122 + - key 1123 + type: object 1124 + x-kubernetes-map-type: atomic 1125 + type: object 1126 + required: 1127 + - name 1128 + type: object 1129 + type: array 1130 + hostname: 1131 + description: Hostname is the public hostname for the Knot server (e.g., 1132 + knot.example.com) 1133 + type: string 1134 + image: 1135 + default: docker.io/tngl/knot:v1.10.0-alpha 1136 + description: Image is the container image to use for the Knot server 1137 + type: string 1138 + imagePullPolicy: 1139 + default: IfNotPresent 1140 + description: ImagePullPolicy defines the pull policy for the container 1141 + image 1142 + type: string 1143 + ingress: 1144 + description: Ingress configures external access (Kubernetes Ingress) 1145 + properties: 1146 + annotations: 1147 + additionalProperties: 1148 + type: string 1149 + description: Annotations to add to the Ingress 1150 + type: object 1151 + enabled: 1152 + description: Enabled enables Ingress creation 1153 + type: boolean 1154 + ingressClassName: 1155 + description: IngressClassName is the IngressClass to use 1156 + type: string 1157 + tls: 1158 + description: TLS configures TLS for the Ingress 1159 + properties: 1160 + enabled: 1161 + description: Enabled enables TLS 1162 + type: boolean 1163 + secretName: 1164 + description: SecretName is the name of the TLS secret 1165 + type: string 1166 + type: object 1167 + type: object 1168 + nodeSelector: 1169 + additionalProperties: 1170 + type: string 1171 + description: NodeSelector for pod scheduling 1172 + type: object 1173 + openshift: 1174 + description: OpenShift contains OpenShift-specific configuration 1175 + properties: 1176 + route: 1177 + description: Route configures OpenShift Route creation 1178 + properties: 1179 + annotations: 1180 + additionalProperties: 1181 + type: string 1182 + description: Annotations to add to the Route 1183 + type: object 1184 + enabled: 1185 + description: Enabled enables Route creation 1186 + type: boolean 1187 + tls: 1188 + description: TLS configures TLS termination for the Route 1189 + properties: 1190 + caCertificate: 1191 + description: CACertificate is the PEM-encoded CA certificate 1192 + type: string 1193 + certificate: 1194 + description: Certificate is the PEM-encoded certificate 1195 + type: string 1196 + destinationCACertificate: 1197 + description: DestinationCACertificate is used for reencrypt 1198 + termination 1199 + type: string 1200 + insecureEdgeTerminationPolicy: 1201 + default: Redirect 1202 + description: InsecureEdgeTerminationPolicy specifies behavior 1203 + for insecure connections 1204 + enum: 1205 + - Allow 1206 + - Redirect 1207 + - None 1208 + type: string 1209 + key: 1210 + description: Key is the PEM-encoded private key 1211 + type: string 1212 + termination: 1213 + default: edge 1214 + description: Termination specifies the TLS termination 1215 + type (edge, passthrough, reencrypt) 1216 + enum: 1217 + - edge 1218 + - passthrough 1219 + - reencrypt 1220 + type: string 1221 + type: object 1222 + wildcardPolicy: 1223 + default: None 1224 + description: WildcardPolicy specifies the wildcard policy 1225 + (None, Subdomain) 1226 + type: string 1227 + type: object 1228 + scc: 1229 + description: SCC configures Security Context Constraints 1230 + properties: 1231 + allowHostIPC: 1232 + default: false 1233 + description: AllowHostIPC allows host IPC namespace 1234 + type: boolean 1235 + allowHostNetwork: 1236 + default: false 1237 + description: AllowHostNetwork allows host network access 1238 + type: boolean 1239 + allowHostPID: 1240 + default: false 1241 + description: AllowHostPID allows host PID namespace 1242 + type: boolean 1243 + allowHostPorts: 1244 + default: false 1245 + description: AllowHostPorts allows host port binding 1246 + type: boolean 1247 + allowPrivilegedContainer: 1248 + default: false 1249 + description: AllowPrivilegedContainer allows privileged containers 1250 + type: boolean 1251 + create: 1252 + description: Create specifies whether to create a custom SCC 1253 + type: boolean 1254 + fsGroup: 1255 + default: MustRunAs 1256 + description: FSGroup specifies the fs group strategy 1257 + type: string 1258 + name: 1259 + default: knot-scc 1260 + description: Name is the name of the SCC to use or create 1261 + type: string 1262 + readOnlyRootFilesystem: 1263 + default: false 1264 + description: ReadOnlyRootFilesystem requires read-only root 1265 + filesystem 1266 + type: boolean 1267 + runAsUser: 1268 + default: MustRunAsNonRoot 1269 + description: RunAsUser specifies the run as user strategy 1270 + type: string 1271 + seLinuxContext: 1272 + default: MustRunAs 1273 + description: SELinuxContext specifies the SELinux context 1274 + strategy 1275 + type: string 1276 + supplementalGroups: 1277 + default: RunAsAny 1278 + description: SupplementalGroups specifies the supplemental 1279 + groups strategy 1280 + type: string 1281 + volumes: 1282 + description: Volumes specifies allowed volume types 1283 + items: 1284 + type: string 1285 + type: array 1286 + type: object 1287 + type: object 1288 + owner: 1289 + description: Owner is the DID identifier of the server owner 1290 + type: string 1291 + replicas: 1292 + default: 1 1293 + description: Replicas is the number of Knot server instances to run 1294 + format: int32 1295 + minimum: 1 1296 + type: integer 1297 + resources: 1298 + description: Resources defines compute resource requirements 1299 + properties: 1300 + claims: 1301 + description: |- 1302 + Claims lists the names of resources, defined in spec.resourceClaims, 1303 + that are used by this container. 1304 + 1305 + This field depends on the 1306 + DynamicResourceAllocation feature gate. 1307 + 1308 + This field is immutable. It can only be set for containers. 1309 + items: 1310 + description: ResourceClaim references one entry in PodSpec.ResourceClaims. 1311 + properties: 1312 + name: 1313 + description: |- 1314 + Name must match the name of one entry in pod.spec.resourceClaims of 1315 + the Pod where this field is used. It makes that resource available 1316 + inside a container. 1317 + type: string 1318 + request: 1319 + description: |- 1320 + Request is the name chosen for a request in the referenced claim. 1321 + If empty, everything from the claim is made available, otherwise 1322 + only the result of this request. 1323 + type: string 1324 + required: 1325 + - name 1326 + type: object 1327 + type: array 1328 + x-kubernetes-list-map-keys: 1329 + - name 1330 + x-kubernetes-list-type: map 1331 + limits: 1332 + additionalProperties: 1333 + anyOf: 1334 + - type: integer 1335 + - type: string 1336 + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1337 + x-kubernetes-int-or-string: true 1338 + description: |- 1339 + Limits describes the maximum amount of compute resources allowed. 1340 + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 1341 + type: object 1342 + requests: 1343 + additionalProperties: 1344 + anyOf: 1345 + - type: integer 1346 + - type: string 1347 + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1348 + x-kubernetes-int-or-string: true 1349 + description: |- 1350 + Requests describes the minimum amount of compute resources required. 1351 + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, 1352 + otherwise to an implementation-defined value. Requests cannot exceed Limits. 1353 + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 1354 + type: object 1355 + type: object 1356 + serviceAccountName: 1357 + description: ServiceAccountName is the name of the ServiceAccount 1358 + to use 1359 + type: string 1360 + ssh: 1361 + description: SSH configures the SSH server for git operations 1362 + properties: 1363 + annotations: 1364 + additionalProperties: 1365 + type: string 1366 + description: Annotations to add to the SSH Service 1367 + type: object 1368 + enabled: 1369 + description: Enabled enables SSH access for git operations 1370 + type: boolean 1371 + loadBalancerIP: 1372 + description: LoadBalancerIP is the static IP for LoadBalancer 1373 + type services 1374 + type: string 1375 + nodePort: 1376 + description: NodePort is the port to use when ServiceType is NodePort 1377 + format: int32 1378 + type: integer 1379 + port: 1380 + default: 22 1381 + description: Port is the SSH port to expose 1382 + format: int32 1383 + type: integer 1384 + serviceType: 1385 + default: LoadBalancer 1386 + description: ServiceType is the Kubernetes Service type for SSH 1387 + type: string 1388 + type: object 1389 + storage: 1390 + description: Storage configures persistent storage for repositories 1391 + and database 1392 + properties: 1393 + dbPath: 1394 + default: /data/db 1395 + description: DBPath is the path where the database is stored 1396 + type: string 1397 + dbSize: 1398 + default: 1Gi 1399 + description: DBSize is the size of the database PVC 1400 + type: string 1401 + dbStorageClass: 1402 + description: DBStorageClass is the StorageClass for database storage 1403 + type: string 1404 + repoPath: 1405 + default: /data/repos 1406 + description: RepoPath is the path where repositories are stored 1407 + type: string 1408 + repoSize: 1409 + default: 10Gi 1410 + description: RepoSize is the size of the repository PVC 1411 + type: string 1412 + repoStorageClass: 1413 + description: RepoStorageClass is the StorageClass for repository 1414 + storage 1415 + type: string 1416 + type: object 1417 + tolerations: 1418 + description: Tolerations for pod scheduling 1419 + items: 1420 + description: |- 1421 + The pod this Toleration is attached to tolerates any taint that matches 1422 + the triple <key,value,effect> using the matching operator <operator>. 1423 + properties: 1424 + effect: 1425 + description: |- 1426 + Effect indicates the taint effect to match. Empty means match all taint effects. 1427 + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. 1428 + type: string 1429 + key: 1430 + description: |- 1431 + Key is the taint key that the toleration applies to. Empty means match all taint keys. 1432 + If the key is empty, operator must be Exists; this combination means to match all values and all keys. 1433 + type: string 1434 + operator: 1435 + description: |- 1436 + Operator represents a key's relationship to the value. 1437 + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. 1438 + Exists is equivalent to wildcard for value, so that a pod can 1439 + tolerate all taints of a particular category. 1440 + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). 1441 + type: string 1442 + tolerationSeconds: 1443 + description: |- 1444 + TolerationSeconds represents the period of time the toleration (which must be 1445 + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, 1446 + it is not set, which means tolerate the taint forever (do not evict). Zero and 1447 + negative values will be treated as 0 (evict immediately) by the system. 1448 + format: int64 1449 + type: integer 1450 + value: 1451 + description: |- 1452 + Value is the taint value the toleration matches to. 1453 + If the operator is Exists, the value should be empty, otherwise just a regular string. 1454 + type: string 1455 + type: object 1456 + type: array 1457 + required: 1458 + - hostname 1459 + - owner 1460 + type: object 1461 + status: 1462 + description: KnotStatus defines the observed state of Knot 1463 + properties: 1464 + availableReplicas: 1465 + description: AvailableReplicas is the number of available replicas 1466 + format: int32 1467 + type: integer 1468 + conditions: 1469 + description: Conditions represent the latest available observations 1470 + items: 1471 + description: Condition contains details for one aspect of the current 1472 + state of this API Resource. 1473 + properties: 1474 + lastTransitionTime: 1475 + description: |- 1476 + lastTransitionTime is the last time the condition transitioned from one status to another. 1477 + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 1478 + format: date-time 1479 + type: string 1480 + message: 1481 + description: |- 1482 + message is a human readable message indicating details about the transition. 1483 + This may be an empty string. 1484 + maxLength: 32768 1485 + type: string 1486 + observedGeneration: 1487 + description: |- 1488 + observedGeneration represents the .metadata.generation that the condition was set based upon. 1489 + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 1490 + with respect to the current state of the instance. 1491 + format: int64 1492 + minimum: 0 1493 + type: integer 1494 + reason: 1495 + description: |- 1496 + reason contains a programmatic identifier indicating the reason for the condition's last transition. 1497 + Producers of specific condition types may define expected values and meanings for this field, 1498 + and whether the values are considered a guaranteed API. 1499 + The value should be a CamelCase string. 1500 + This field may not be empty. 1501 + maxLength: 1024 1502 + minLength: 1 1503 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 1504 + type: string 1505 + status: 1506 + description: status of the condition, one of True, False, Unknown. 1507 + enum: 1508 + - "True" 1509 + - "False" 1510 + - Unknown 1511 + type: string 1512 + type: 1513 + description: type of condition in CamelCase or in foo.example.com/CamelCase. 1514 + maxLength: 316 1515 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 1516 + type: string 1517 + required: 1518 + - lastTransitionTime 1519 + - message 1520 + - reason 1521 + - status 1522 + - type 1523 + type: object 1524 + type: array 1525 + observedGeneration: 1526 + description: ObservedGeneration is the most recent generation observed 1527 + format: int64 1528 + type: integer 1529 + phase: 1530 + description: Phase represents the current phase of the Knot deployment 1531 + enum: 1532 + - Pending 1533 + - Running 1534 + - Failed 1535 + - Unknown 1536 + type: string 1537 + readyReplicas: 1538 + description: ReadyReplicas is the number of ready replicas 1539 + format: int32 1540 + type: integer 1541 + sshURL: 1542 + description: SSHURL is the SSH URL for git operations 1543 + type: string 1544 + url: 1545 + description: URL is the external URL of the Knot server 1546 + type: string 1547 + type: object 1548 + type: object 1549 + served: true 1550 + storage: true 1551 + subresources: 1552 + status: {}
+4
config/crd/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + resources: 4 + - tangled.org_knots.yaml
+1552
config/crd/tangled.org_knots.yaml
··· 1 + --- 2 + apiVersion: apiextensions.k8s.io/v1 3 + kind: CustomResourceDefinition 4 + metadata: 5 + annotations: 6 + controller-gen.kubebuilder.io/version: v0.20.0 7 + name: knots.tangled.org 8 + spec: 9 + group: tangled.org 10 + names: 11 + kind: Knot 12 + listKind: KnotList 13 + plural: knots 14 + shortNames: 15 + - kt 16 + singular: knot 17 + scope: Namespaced 18 + versions: 19 + - additionalPrinterColumns: 20 + - jsonPath: .status.phase 21 + name: Phase 22 + type: string 23 + - jsonPath: .status.url 24 + name: URL 25 + type: string 26 + - jsonPath: .status.readyReplicas 27 + name: Ready 28 + type: integer 29 + - jsonPath: .metadata.creationTimestamp 30 + name: Age 31 + type: date 32 + name: v1alpha1 33 + schema: 34 + openAPIV3Schema: 35 + description: Knot is the Schema for the knots API 36 + properties: 37 + apiVersion: 38 + description: |- 39 + APIVersion defines the versioned schema of this representation of an object. 40 + Servers should convert recognized schemas to the latest internal value, and 41 + may reject unrecognized values. 42 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 43 + type: string 44 + kind: 45 + description: |- 46 + Kind is a string value representing the REST resource this object represents. 47 + Servers may infer this from the endpoint the client submits requests to. 48 + Cannot be updated. 49 + In CamelCase. 50 + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 51 + type: string 52 + metadata: 53 + type: object 54 + spec: 55 + description: KnotSpec defines the desired state of Knot 56 + properties: 57 + affinity: 58 + description: Affinity rules for pod scheduling 59 + properties: 60 + nodeAffinity: 61 + description: Describes node affinity scheduling rules for the 62 + pod. 63 + properties: 64 + preferredDuringSchedulingIgnoredDuringExecution: 65 + description: |- 66 + The scheduler will prefer to schedule pods to nodes that satisfy 67 + the affinity expressions specified by this field, but it may choose 68 + a node that violates one or more of the expressions. The node that is 69 + most preferred is the one with the greatest sum of weights, i.e. 70 + for each node that meets all of the scheduling requirements (resource 71 + request, requiredDuringScheduling affinity expressions, etc.), 72 + compute a sum by iterating through the elements of this field and adding 73 + "weight" to the sum if the node matches the corresponding matchExpressions; the 74 + node(s) with the highest sum are the most preferred. 75 + items: 76 + description: |- 77 + An empty preferred scheduling term matches all objects with implicit weight 0 78 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). 79 + properties: 80 + preference: 81 + description: A node selector term, associated with the 82 + corresponding weight. 83 + properties: 84 + matchExpressions: 85 + description: A list of node selector requirements 86 + by node's labels. 87 + items: 88 + description: |- 89 + A node selector requirement is a selector that contains values, a key, and an operator 90 + that relates the key and values. 91 + properties: 92 + key: 93 + description: The label key that the selector 94 + applies to. 95 + type: string 96 + operator: 97 + description: |- 98 + Represents a key's relationship to a set of values. 99 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 100 + type: string 101 + values: 102 + description: |- 103 + An array of string values. If the operator is In or NotIn, 104 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 105 + the values array must be empty. If the operator is Gt or Lt, the values 106 + array must have a single element, which will be interpreted as an integer. 107 + This array is replaced during a strategic merge patch. 108 + items: 109 + type: string 110 + type: array 111 + x-kubernetes-list-type: atomic 112 + required: 113 + - key 114 + - operator 115 + type: object 116 + type: array 117 + x-kubernetes-list-type: atomic 118 + matchFields: 119 + description: A list of node selector requirements 120 + by node's fields. 121 + items: 122 + description: |- 123 + A node selector requirement is a selector that contains values, a key, and an operator 124 + that relates the key and values. 125 + properties: 126 + key: 127 + description: The label key that the selector 128 + applies to. 129 + type: string 130 + operator: 131 + description: |- 132 + Represents a key's relationship to a set of values. 133 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 134 + type: string 135 + values: 136 + description: |- 137 + An array of string values. If the operator is In or NotIn, 138 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 139 + the values array must be empty. If the operator is Gt or Lt, the values 140 + array must have a single element, which will be interpreted as an integer. 141 + This array is replaced during a strategic merge patch. 142 + items: 143 + type: string 144 + type: array 145 + x-kubernetes-list-type: atomic 146 + required: 147 + - key 148 + - operator 149 + type: object 150 + type: array 151 + x-kubernetes-list-type: atomic 152 + type: object 153 + x-kubernetes-map-type: atomic 154 + weight: 155 + description: Weight associated with matching the corresponding 156 + nodeSelectorTerm, in the range 1-100. 157 + format: int32 158 + type: integer 159 + required: 160 + - preference 161 + - weight 162 + type: object 163 + type: array 164 + x-kubernetes-list-type: atomic 165 + requiredDuringSchedulingIgnoredDuringExecution: 166 + description: |- 167 + If the affinity requirements specified by this field are not met at 168 + scheduling time, the pod will not be scheduled onto the node. 169 + If the affinity requirements specified by this field cease to be met 170 + at some point during pod execution (e.g. due to an update), the system 171 + may or may not try to eventually evict the pod from its node. 172 + properties: 173 + nodeSelectorTerms: 174 + description: Required. A list of node selector terms. 175 + The terms are ORed. 176 + items: 177 + description: |- 178 + A null or empty node selector term matches no objects. The requirements of 179 + them are ANDed. 180 + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. 181 + properties: 182 + matchExpressions: 183 + description: A list of node selector requirements 184 + by node's labels. 185 + items: 186 + description: |- 187 + A node selector requirement is a selector that contains values, a key, and an operator 188 + that relates the key and values. 189 + properties: 190 + key: 191 + description: The label key that the selector 192 + applies to. 193 + type: string 194 + operator: 195 + description: |- 196 + Represents a key's relationship to a set of values. 197 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 198 + type: string 199 + values: 200 + description: |- 201 + An array of string values. If the operator is In or NotIn, 202 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 203 + the values array must be empty. If the operator is Gt or Lt, the values 204 + array must have a single element, which will be interpreted as an integer. 205 + This array is replaced during a strategic merge patch. 206 + items: 207 + type: string 208 + type: array 209 + x-kubernetes-list-type: atomic 210 + required: 211 + - key 212 + - operator 213 + type: object 214 + type: array 215 + x-kubernetes-list-type: atomic 216 + matchFields: 217 + description: A list of node selector requirements 218 + by node's fields. 219 + items: 220 + description: |- 221 + A node selector requirement is a selector that contains values, a key, and an operator 222 + that relates the key and values. 223 + properties: 224 + key: 225 + description: The label key that the selector 226 + applies to. 227 + type: string 228 + operator: 229 + description: |- 230 + Represents a key's relationship to a set of values. 231 + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. 232 + type: string 233 + values: 234 + description: |- 235 + An array of string values. If the operator is In or NotIn, 236 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 237 + the values array must be empty. If the operator is Gt or Lt, the values 238 + array must have a single element, which will be interpreted as an integer. 239 + This array is replaced during a strategic merge patch. 240 + items: 241 + type: string 242 + type: array 243 + x-kubernetes-list-type: atomic 244 + required: 245 + - key 246 + - operator 247 + type: object 248 + type: array 249 + x-kubernetes-list-type: atomic 250 + type: object 251 + x-kubernetes-map-type: atomic 252 + type: array 253 + x-kubernetes-list-type: atomic 254 + required: 255 + - nodeSelectorTerms 256 + type: object 257 + x-kubernetes-map-type: atomic 258 + type: object 259 + podAffinity: 260 + description: Describes pod affinity scheduling rules (e.g. co-locate 261 + this pod in the same node, zone, etc. as some other pod(s)). 262 + properties: 263 + preferredDuringSchedulingIgnoredDuringExecution: 264 + description: |- 265 + The scheduler will prefer to schedule pods to nodes that satisfy 266 + the affinity expressions specified by this field, but it may choose 267 + a node that violates one or more of the expressions. The node that is 268 + most preferred is the one with the greatest sum of weights, i.e. 269 + for each node that meets all of the scheduling requirements (resource 270 + request, requiredDuringScheduling affinity expressions, etc.), 271 + compute a sum by iterating through the elements of this field and adding 272 + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the 273 + node(s) with the highest sum are the most preferred. 274 + items: 275 + description: The weights of all of the matched WeightedPodAffinityTerm 276 + fields are added per-node to find the most preferred node(s) 277 + properties: 278 + podAffinityTerm: 279 + description: Required. A pod affinity term, associated 280 + with the corresponding weight. 281 + properties: 282 + labelSelector: 283 + description: |- 284 + A label query over a set of resources, in this case pods. 285 + If it's null, this PodAffinityTerm matches with no Pods. 286 + properties: 287 + matchExpressions: 288 + description: matchExpressions is a list of label 289 + selector requirements. The requirements are 290 + ANDed. 291 + items: 292 + description: |- 293 + A label selector requirement is a selector that contains values, a key, and an operator that 294 + relates the key and values. 295 + properties: 296 + key: 297 + description: key is the label key that 298 + the selector applies to. 299 + type: string 300 + operator: 301 + description: |- 302 + operator represents a key's relationship to a set of values. 303 + Valid operators are In, NotIn, Exists and DoesNotExist. 304 + type: string 305 + values: 306 + description: |- 307 + values is an array of string values. If the operator is In or NotIn, 308 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 309 + the values array must be empty. This array is replaced during a strategic 310 + merge patch. 311 + items: 312 + type: string 313 + type: array 314 + x-kubernetes-list-type: atomic 315 + required: 316 + - key 317 + - operator 318 + type: object 319 + type: array 320 + x-kubernetes-list-type: atomic 321 + matchLabels: 322 + additionalProperties: 323 + type: string 324 + description: |- 325 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 326 + map is equivalent to an element of matchExpressions, whose key field is "key", the 327 + operator is "In", and the values array contains only "value". The requirements are ANDed. 328 + type: object 329 + type: object 330 + x-kubernetes-map-type: atomic 331 + matchLabelKeys: 332 + description: |- 333 + MatchLabelKeys is a set of pod label keys to select which pods will 334 + be taken into consideration. The keys are used to lookup values from the 335 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 336 + to select the group of existing pods which pods will be taken into consideration 337 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 338 + pod labels will be ignored. The default value is empty. 339 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 340 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 341 + items: 342 + type: string 343 + type: array 344 + x-kubernetes-list-type: atomic 345 + mismatchLabelKeys: 346 + description: |- 347 + MismatchLabelKeys is a set of pod label keys to select which pods will 348 + be taken into consideration. The keys are used to lookup values from the 349 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 350 + to select the group of existing pods which pods will be taken into consideration 351 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 352 + pod labels will be ignored. The default value is empty. 353 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 354 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 355 + items: 356 + type: string 357 + type: array 358 + x-kubernetes-list-type: atomic 359 + namespaceSelector: 360 + description: |- 361 + A label query over the set of namespaces that the term applies to. 362 + The term is applied to the union of the namespaces selected by this field 363 + and the ones listed in the namespaces field. 364 + null selector and null or empty namespaces list means "this pod's namespace". 365 + An empty selector ({}) matches all namespaces. 366 + properties: 367 + matchExpressions: 368 + description: matchExpressions is a list of label 369 + selector requirements. The requirements are 370 + ANDed. 371 + items: 372 + description: |- 373 + A label selector requirement is a selector that contains values, a key, and an operator that 374 + relates the key and values. 375 + properties: 376 + key: 377 + description: key is the label key that 378 + the selector applies to. 379 + type: string 380 + operator: 381 + description: |- 382 + operator represents a key's relationship to a set of values. 383 + Valid operators are In, NotIn, Exists and DoesNotExist. 384 + type: string 385 + values: 386 + description: |- 387 + values is an array of string values. If the operator is In or NotIn, 388 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 389 + the values array must be empty. This array is replaced during a strategic 390 + merge patch. 391 + items: 392 + type: string 393 + type: array 394 + x-kubernetes-list-type: atomic 395 + required: 396 + - key 397 + - operator 398 + type: object 399 + type: array 400 + x-kubernetes-list-type: atomic 401 + matchLabels: 402 + additionalProperties: 403 + type: string 404 + description: |- 405 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 406 + map is equivalent to an element of matchExpressions, whose key field is "key", the 407 + operator is "In", and the values array contains only "value". The requirements are ANDed. 408 + type: object 409 + type: object 410 + x-kubernetes-map-type: atomic 411 + namespaces: 412 + description: |- 413 + namespaces specifies a static list of namespace names that the term applies to. 414 + The term is applied to the union of the namespaces listed in this field 415 + and the ones selected by namespaceSelector. 416 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 417 + items: 418 + type: string 419 + type: array 420 + x-kubernetes-list-type: atomic 421 + topologyKey: 422 + description: |- 423 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 424 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 425 + whose value of the label with key topologyKey matches that of any node on which any of the 426 + selected pods is running. 427 + Empty topologyKey is not allowed. 428 + type: string 429 + required: 430 + - topologyKey 431 + type: object 432 + weight: 433 + description: |- 434 + weight associated with matching the corresponding podAffinityTerm, 435 + in the range 1-100. 436 + format: int32 437 + type: integer 438 + required: 439 + - podAffinityTerm 440 + - weight 441 + type: object 442 + type: array 443 + x-kubernetes-list-type: atomic 444 + requiredDuringSchedulingIgnoredDuringExecution: 445 + description: |- 446 + If the affinity requirements specified by this field are not met at 447 + scheduling time, the pod will not be scheduled onto the node. 448 + If the affinity requirements specified by this field cease to be met 449 + at some point during pod execution (e.g. due to a pod label update), the 450 + system may or may not try to eventually evict the pod from its node. 451 + When there are multiple elements, the lists of nodes corresponding to each 452 + podAffinityTerm are intersected, i.e. all terms must be satisfied. 453 + items: 454 + description: |- 455 + Defines a set of pods (namely those matching the labelSelector 456 + relative to the given namespace(s)) that this pod should be 457 + co-located (affinity) or not co-located (anti-affinity) with, 458 + where co-located is defined as running on a node whose value of 459 + the label with key <topologyKey> matches that of any node on which 460 + a pod of the set of pods is running 461 + properties: 462 + labelSelector: 463 + description: |- 464 + A label query over a set of resources, in this case pods. 465 + If it's null, this PodAffinityTerm matches with no Pods. 466 + properties: 467 + matchExpressions: 468 + description: matchExpressions is a list of label 469 + selector requirements. The requirements are ANDed. 470 + items: 471 + description: |- 472 + A label selector requirement is a selector that contains values, a key, and an operator that 473 + relates the key and values. 474 + properties: 475 + key: 476 + description: key is the label key that the 477 + selector applies to. 478 + type: string 479 + operator: 480 + description: |- 481 + operator represents a key's relationship to a set of values. 482 + Valid operators are In, NotIn, Exists and DoesNotExist. 483 + type: string 484 + values: 485 + description: |- 486 + values is an array of string values. If the operator is In or NotIn, 487 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 488 + the values array must be empty. This array is replaced during a strategic 489 + merge patch. 490 + items: 491 + type: string 492 + type: array 493 + x-kubernetes-list-type: atomic 494 + required: 495 + - key 496 + - operator 497 + type: object 498 + type: array 499 + x-kubernetes-list-type: atomic 500 + matchLabels: 501 + additionalProperties: 502 + type: string 503 + description: |- 504 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 505 + map is equivalent to an element of matchExpressions, whose key field is "key", the 506 + operator is "In", and the values array contains only "value". The requirements are ANDed. 507 + type: object 508 + type: object 509 + x-kubernetes-map-type: atomic 510 + matchLabelKeys: 511 + description: |- 512 + MatchLabelKeys is a set of pod label keys to select which pods will 513 + be taken into consideration. The keys are used to lookup values from the 514 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 515 + to select the group of existing pods which pods will be taken into consideration 516 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 517 + pod labels will be ignored. The default value is empty. 518 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 519 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 520 + items: 521 + type: string 522 + type: array 523 + x-kubernetes-list-type: atomic 524 + mismatchLabelKeys: 525 + description: |- 526 + MismatchLabelKeys is a set of pod label keys to select which pods will 527 + be taken into consideration. The keys are used to lookup values from the 528 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 529 + to select the group of existing pods which pods will be taken into consideration 530 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 531 + pod labels will be ignored. The default value is empty. 532 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 533 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 534 + items: 535 + type: string 536 + type: array 537 + x-kubernetes-list-type: atomic 538 + namespaceSelector: 539 + description: |- 540 + A label query over the set of namespaces that the term applies to. 541 + The term is applied to the union of the namespaces selected by this field 542 + and the ones listed in the namespaces field. 543 + null selector and null or empty namespaces list means "this pod's namespace". 544 + An empty selector ({}) matches all namespaces. 545 + properties: 546 + matchExpressions: 547 + description: matchExpressions is a list of label 548 + selector requirements. The requirements are ANDed. 549 + items: 550 + description: |- 551 + A label selector requirement is a selector that contains values, a key, and an operator that 552 + relates the key and values. 553 + properties: 554 + key: 555 + description: key is the label key that the 556 + selector applies to. 557 + type: string 558 + operator: 559 + description: |- 560 + operator represents a key's relationship to a set of values. 561 + Valid operators are In, NotIn, Exists and DoesNotExist. 562 + type: string 563 + values: 564 + description: |- 565 + values is an array of string values. If the operator is In or NotIn, 566 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 567 + the values array must be empty. This array is replaced during a strategic 568 + merge patch. 569 + items: 570 + type: string 571 + type: array 572 + x-kubernetes-list-type: atomic 573 + required: 574 + - key 575 + - operator 576 + type: object 577 + type: array 578 + x-kubernetes-list-type: atomic 579 + matchLabels: 580 + additionalProperties: 581 + type: string 582 + description: |- 583 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 584 + map is equivalent to an element of matchExpressions, whose key field is "key", the 585 + operator is "In", and the values array contains only "value". The requirements are ANDed. 586 + type: object 587 + type: object 588 + x-kubernetes-map-type: atomic 589 + namespaces: 590 + description: |- 591 + namespaces specifies a static list of namespace names that the term applies to. 592 + The term is applied to the union of the namespaces listed in this field 593 + and the ones selected by namespaceSelector. 594 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 595 + items: 596 + type: string 597 + type: array 598 + x-kubernetes-list-type: atomic 599 + topologyKey: 600 + description: |- 601 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 602 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 603 + whose value of the label with key topologyKey matches that of any node on which any of the 604 + selected pods is running. 605 + Empty topologyKey is not allowed. 606 + type: string 607 + required: 608 + - topologyKey 609 + type: object 610 + type: array 611 + x-kubernetes-list-type: atomic 612 + type: object 613 + podAntiAffinity: 614 + description: Describes pod anti-affinity scheduling rules (e.g. 615 + avoid putting this pod in the same node, zone, etc. as some 616 + other pod(s)). 617 + properties: 618 + preferredDuringSchedulingIgnoredDuringExecution: 619 + description: |- 620 + The scheduler will prefer to schedule pods to nodes that satisfy 621 + the anti-affinity expressions specified by this field, but it may choose 622 + a node that violates one or more of the expressions. The node that is 623 + most preferred is the one with the greatest sum of weights, i.e. 624 + for each node that meets all of the scheduling requirements (resource 625 + request, requiredDuringScheduling anti-affinity expressions, etc.), 626 + compute a sum by iterating through the elements of this field and subtracting 627 + "weight" from the sum if the node has pods which matches the corresponding podAffinityTerm; the 628 + node(s) with the highest sum are the most preferred. 629 + items: 630 + description: The weights of all of the matched WeightedPodAffinityTerm 631 + fields are added per-node to find the most preferred node(s) 632 + properties: 633 + podAffinityTerm: 634 + description: Required. A pod affinity term, associated 635 + with the corresponding weight. 636 + properties: 637 + labelSelector: 638 + description: |- 639 + A label query over a set of resources, in this case pods. 640 + If it's null, this PodAffinityTerm matches with no Pods. 641 + properties: 642 + matchExpressions: 643 + description: matchExpressions is a list of label 644 + selector requirements. The requirements are 645 + ANDed. 646 + items: 647 + description: |- 648 + A label selector requirement is a selector that contains values, a key, and an operator that 649 + relates the key and values. 650 + properties: 651 + key: 652 + description: key is the label key that 653 + the selector applies to. 654 + type: string 655 + operator: 656 + description: |- 657 + operator represents a key's relationship to a set of values. 658 + Valid operators are In, NotIn, Exists and DoesNotExist. 659 + type: string 660 + values: 661 + description: |- 662 + values is an array of string values. If the operator is In or NotIn, 663 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 664 + the values array must be empty. This array is replaced during a strategic 665 + merge patch. 666 + items: 667 + type: string 668 + type: array 669 + x-kubernetes-list-type: atomic 670 + required: 671 + - key 672 + - operator 673 + type: object 674 + type: array 675 + x-kubernetes-list-type: atomic 676 + matchLabels: 677 + additionalProperties: 678 + type: string 679 + description: |- 680 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 681 + map is equivalent to an element of matchExpressions, whose key field is "key", the 682 + operator is "In", and the values array contains only "value". The requirements are ANDed. 683 + type: object 684 + type: object 685 + x-kubernetes-map-type: atomic 686 + matchLabelKeys: 687 + description: |- 688 + MatchLabelKeys is a set of pod label keys to select which pods will 689 + be taken into consideration. The keys are used to lookup values from the 690 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 691 + to select the group of existing pods which pods will be taken into consideration 692 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 693 + pod labels will be ignored. The default value is empty. 694 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 695 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 696 + items: 697 + type: string 698 + type: array 699 + x-kubernetes-list-type: atomic 700 + mismatchLabelKeys: 701 + description: |- 702 + MismatchLabelKeys is a set of pod label keys to select which pods will 703 + be taken into consideration. The keys are used to lookup values from the 704 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 705 + to select the group of existing pods which pods will be taken into consideration 706 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 707 + pod labels will be ignored. The default value is empty. 708 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 709 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 710 + items: 711 + type: string 712 + type: array 713 + x-kubernetes-list-type: atomic 714 + namespaceSelector: 715 + description: |- 716 + A label query over the set of namespaces that the term applies to. 717 + The term is applied to the union of the namespaces selected by this field 718 + and the ones listed in the namespaces field. 719 + null selector and null or empty namespaces list means "this pod's namespace". 720 + An empty selector ({}) matches all namespaces. 721 + properties: 722 + matchExpressions: 723 + description: matchExpressions is a list of label 724 + selector requirements. The requirements are 725 + ANDed. 726 + items: 727 + description: |- 728 + A label selector requirement is a selector that contains values, a key, and an operator that 729 + relates the key and values. 730 + properties: 731 + key: 732 + description: key is the label key that 733 + the selector applies to. 734 + type: string 735 + operator: 736 + description: |- 737 + operator represents a key's relationship to a set of values. 738 + Valid operators are In, NotIn, Exists and DoesNotExist. 739 + type: string 740 + values: 741 + description: |- 742 + values is an array of string values. If the operator is In or NotIn, 743 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 744 + the values array must be empty. This array is replaced during a strategic 745 + merge patch. 746 + items: 747 + type: string 748 + type: array 749 + x-kubernetes-list-type: atomic 750 + required: 751 + - key 752 + - operator 753 + type: object 754 + type: array 755 + x-kubernetes-list-type: atomic 756 + matchLabels: 757 + additionalProperties: 758 + type: string 759 + description: |- 760 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 761 + map is equivalent to an element of matchExpressions, whose key field is "key", the 762 + operator is "In", and the values array contains only "value". The requirements are ANDed. 763 + type: object 764 + type: object 765 + x-kubernetes-map-type: atomic 766 + namespaces: 767 + description: |- 768 + namespaces specifies a static list of namespace names that the term applies to. 769 + The term is applied to the union of the namespaces listed in this field 770 + and the ones selected by namespaceSelector. 771 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 772 + items: 773 + type: string 774 + type: array 775 + x-kubernetes-list-type: atomic 776 + topologyKey: 777 + description: |- 778 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 779 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 780 + whose value of the label with key topologyKey matches that of any node on which any of the 781 + selected pods is running. 782 + Empty topologyKey is not allowed. 783 + type: string 784 + required: 785 + - topologyKey 786 + type: object 787 + weight: 788 + description: |- 789 + weight associated with matching the corresponding podAffinityTerm, 790 + in the range 1-100. 791 + format: int32 792 + type: integer 793 + required: 794 + - podAffinityTerm 795 + - weight 796 + type: object 797 + type: array 798 + x-kubernetes-list-type: atomic 799 + requiredDuringSchedulingIgnoredDuringExecution: 800 + description: |- 801 + If the anti-affinity requirements specified by this field are not met at 802 + scheduling time, the pod will not be scheduled onto the node. 803 + If the anti-affinity requirements specified by this field cease to be met 804 + at some point during pod execution (e.g. due to a pod label update), the 805 + system may or may not try to eventually evict the pod from its node. 806 + When there are multiple elements, the lists of nodes corresponding to each 807 + podAffinityTerm are intersected, i.e. all terms must be satisfied. 808 + items: 809 + description: |- 810 + Defines a set of pods (namely those matching the labelSelector 811 + relative to the given namespace(s)) that this pod should be 812 + co-located (affinity) or not co-located (anti-affinity) with, 813 + where co-located is defined as running on a node whose value of 814 + the label with key <topologyKey> matches that of any node on which 815 + a pod of the set of pods is running 816 + properties: 817 + labelSelector: 818 + description: |- 819 + A label query over a set of resources, in this case pods. 820 + If it's null, this PodAffinityTerm matches with no Pods. 821 + properties: 822 + matchExpressions: 823 + description: matchExpressions is a list of label 824 + selector requirements. The requirements are ANDed. 825 + items: 826 + description: |- 827 + A label selector requirement is a selector that contains values, a key, and an operator that 828 + relates the key and values. 829 + properties: 830 + key: 831 + description: key is the label key that the 832 + selector applies to. 833 + type: string 834 + operator: 835 + description: |- 836 + operator represents a key's relationship to a set of values. 837 + Valid operators are In, NotIn, Exists and DoesNotExist. 838 + type: string 839 + values: 840 + description: |- 841 + values is an array of string values. If the operator is In or NotIn, 842 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 843 + the values array must be empty. This array is replaced during a strategic 844 + merge patch. 845 + items: 846 + type: string 847 + type: array 848 + x-kubernetes-list-type: atomic 849 + required: 850 + - key 851 + - operator 852 + type: object 853 + type: array 854 + x-kubernetes-list-type: atomic 855 + matchLabels: 856 + additionalProperties: 857 + type: string 858 + description: |- 859 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 860 + map is equivalent to an element of matchExpressions, whose key field is "key", the 861 + operator is "In", and the values array contains only "value". The requirements are ANDed. 862 + type: object 863 + type: object 864 + x-kubernetes-map-type: atomic 865 + matchLabelKeys: 866 + description: |- 867 + MatchLabelKeys is a set of pod label keys to select which pods will 868 + be taken into consideration. The keys are used to lookup values from the 869 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` 870 + to select the group of existing pods which pods will be taken into consideration 871 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 872 + pod labels will be ignored. The default value is empty. 873 + The same key is forbidden to exist in both matchLabelKeys and labelSelector. 874 + Also, matchLabelKeys cannot be set when labelSelector isn't set. 875 + items: 876 + type: string 877 + type: array 878 + x-kubernetes-list-type: atomic 879 + mismatchLabelKeys: 880 + description: |- 881 + MismatchLabelKeys is a set of pod label keys to select which pods will 882 + be taken into consideration. The keys are used to lookup values from the 883 + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` 884 + to select the group of existing pods which pods will be taken into consideration 885 + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming 886 + pod labels will be ignored. The default value is empty. 887 + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. 888 + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. 889 + items: 890 + type: string 891 + type: array 892 + x-kubernetes-list-type: atomic 893 + namespaceSelector: 894 + description: |- 895 + A label query over the set of namespaces that the term applies to. 896 + The term is applied to the union of the namespaces selected by this field 897 + and the ones listed in the namespaces field. 898 + null selector and null or empty namespaces list means "this pod's namespace". 899 + An empty selector ({}) matches all namespaces. 900 + properties: 901 + matchExpressions: 902 + description: matchExpressions is a list of label 903 + selector requirements. The requirements are ANDed. 904 + items: 905 + description: |- 906 + A label selector requirement is a selector that contains values, a key, and an operator that 907 + relates the key and values. 908 + properties: 909 + key: 910 + description: key is the label key that the 911 + selector applies to. 912 + type: string 913 + operator: 914 + description: |- 915 + operator represents a key's relationship to a set of values. 916 + Valid operators are In, NotIn, Exists and DoesNotExist. 917 + type: string 918 + values: 919 + description: |- 920 + values is an array of string values. If the operator is In or NotIn, 921 + the values array must be non-empty. If the operator is Exists or DoesNotExist, 922 + the values array must be empty. This array is replaced during a strategic 923 + merge patch. 924 + items: 925 + type: string 926 + type: array 927 + x-kubernetes-list-type: atomic 928 + required: 929 + - key 930 + - operator 931 + type: object 932 + type: array 933 + x-kubernetes-list-type: atomic 934 + matchLabels: 935 + additionalProperties: 936 + type: string 937 + description: |- 938 + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels 939 + map is equivalent to an element of matchExpressions, whose key field is "key", the 940 + operator is "In", and the values array contains only "value". The requirements are ANDed. 941 + type: object 942 + type: object 943 + x-kubernetes-map-type: atomic 944 + namespaces: 945 + description: |- 946 + namespaces specifies a static list of namespace names that the term applies to. 947 + The term is applied to the union of the namespaces listed in this field 948 + and the ones selected by namespaceSelector. 949 + null or empty namespaces list and null namespaceSelector means "this pod's namespace". 950 + items: 951 + type: string 952 + type: array 953 + x-kubernetes-list-type: atomic 954 + topologyKey: 955 + description: |- 956 + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching 957 + the labelSelector in the specified namespaces, where co-located is defined as running on a node 958 + whose value of the label with key topologyKey matches that of any node on which any of the 959 + selected pods is running. 960 + Empty topologyKey is not allowed. 961 + type: string 962 + required: 963 + - topologyKey 964 + type: object 965 + type: array 966 + x-kubernetes-list-type: atomic 967 + type: object 968 + type: object 969 + appviewEndpoint: 970 + default: https://tangled.org 971 + description: AppviewEndpoint is the appview endpoint URL 972 + type: string 973 + extraEnv: 974 + description: ExtraEnv allows adding additional environment variables 975 + items: 976 + description: EnvVar represents an environment variable present in 977 + a Container. 978 + properties: 979 + name: 980 + description: |- 981 + Name of the environment variable. 982 + May consist of any printable ASCII characters except '='. 983 + type: string 984 + value: 985 + description: |- 986 + Variable references $(VAR_NAME) are expanded 987 + using the previously defined environment variables in the container and 988 + any service environment variables. If a variable cannot be resolved, 989 + the reference in the input string will be unchanged. Double $$ are reduced 990 + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. 991 + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". 992 + Escaped references will never be expanded, regardless of whether the variable 993 + exists or not. 994 + Defaults to "". 995 + type: string 996 + valueFrom: 997 + description: Source for the environment variable's value. Cannot 998 + be used if value is not empty. 999 + properties: 1000 + configMapKeyRef: 1001 + description: Selects a key of a ConfigMap. 1002 + properties: 1003 + key: 1004 + description: The key to select. 1005 + type: string 1006 + name: 1007 + default: "" 1008 + description: |- 1009 + Name of the referent. 1010 + This field is effectively required, but due to backwards compatibility is 1011 + allowed to be empty. Instances of this type with an empty value here are 1012 + almost certainly wrong. 1013 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 1014 + type: string 1015 + optional: 1016 + description: Specify whether the ConfigMap or its key 1017 + must be defined 1018 + type: boolean 1019 + required: 1020 + - key 1021 + type: object 1022 + x-kubernetes-map-type: atomic 1023 + fieldRef: 1024 + description: |- 1025 + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`, 1026 + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. 1027 + properties: 1028 + apiVersion: 1029 + description: Version of the schema the FieldPath is 1030 + written in terms of, defaults to "v1". 1031 + type: string 1032 + fieldPath: 1033 + description: Path of the field to select in the specified 1034 + API version. 1035 + type: string 1036 + required: 1037 + - fieldPath 1038 + type: object 1039 + x-kubernetes-map-type: atomic 1040 + fileKeyRef: 1041 + description: |- 1042 + FileKeyRef selects a key of the env file. 1043 + Requires the EnvFiles feature gate to be enabled. 1044 + properties: 1045 + key: 1046 + description: |- 1047 + The key within the env file. An invalid key will prevent the pod from starting. 1048 + The keys defined within a source may consist of any printable ASCII characters except '='. 1049 + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. 1050 + type: string 1051 + optional: 1052 + default: false 1053 + description: |- 1054 + Specify whether the file or its key must be defined. If the file or key 1055 + does not exist, then the env var is not published. 1056 + If optional is set to true and the specified key does not exist, 1057 + the environment variable will not be set in the Pod's containers. 1058 + 1059 + If optional is set to false and the specified key does not exist, 1060 + an error will be returned during Pod creation. 1061 + type: boolean 1062 + path: 1063 + description: |- 1064 + The path within the volume from which to select the file. 1065 + Must be relative and may not contain the '..' path or start with '..'. 1066 + type: string 1067 + volumeName: 1068 + description: The name of the volume mount containing 1069 + the env file. 1070 + type: string 1071 + required: 1072 + - key 1073 + - path 1074 + - volumeName 1075 + type: object 1076 + x-kubernetes-map-type: atomic 1077 + resourceFieldRef: 1078 + description: |- 1079 + Selects a resource of the container: only resources limits and requests 1080 + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. 1081 + properties: 1082 + containerName: 1083 + description: 'Container name: required for volumes, 1084 + optional for env vars' 1085 + type: string 1086 + divisor: 1087 + anyOf: 1088 + - type: integer 1089 + - type: string 1090 + description: Specifies the output format of the exposed 1091 + resources, defaults to "1" 1092 + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1093 + x-kubernetes-int-or-string: true 1094 + resource: 1095 + description: 'Required: resource to select' 1096 + type: string 1097 + required: 1098 + - resource 1099 + type: object 1100 + x-kubernetes-map-type: atomic 1101 + secretKeyRef: 1102 + description: Selects a key of a secret in the pod's namespace 1103 + properties: 1104 + key: 1105 + description: The key of the secret to select from. Must 1106 + be a valid secret key. 1107 + type: string 1108 + name: 1109 + default: "" 1110 + description: |- 1111 + Name of the referent. 1112 + This field is effectively required, but due to backwards compatibility is 1113 + allowed to be empty. Instances of this type with an empty value here are 1114 + almost certainly wrong. 1115 + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 1116 + type: string 1117 + optional: 1118 + description: Specify whether the Secret or its key must 1119 + be defined 1120 + type: boolean 1121 + required: 1122 + - key 1123 + type: object 1124 + x-kubernetes-map-type: atomic 1125 + type: object 1126 + required: 1127 + - name 1128 + type: object 1129 + type: array 1130 + hostname: 1131 + description: Hostname is the public hostname for the Knot server (e.g., 1132 + knot.example.com) 1133 + type: string 1134 + image: 1135 + default: docker.io/tngl/knot:v1.10.0-alpha 1136 + description: Image is the container image to use for the Knot server 1137 + type: string 1138 + imagePullPolicy: 1139 + default: IfNotPresent 1140 + description: ImagePullPolicy defines the pull policy for the container 1141 + image 1142 + type: string 1143 + ingress: 1144 + description: Ingress configures external access (Kubernetes Ingress) 1145 + properties: 1146 + annotations: 1147 + additionalProperties: 1148 + type: string 1149 + description: Annotations to add to the Ingress 1150 + type: object 1151 + enabled: 1152 + description: Enabled enables Ingress creation 1153 + type: boolean 1154 + ingressClassName: 1155 + description: IngressClassName is the IngressClass to use 1156 + type: string 1157 + tls: 1158 + description: TLS configures TLS for the Ingress 1159 + properties: 1160 + enabled: 1161 + description: Enabled enables TLS 1162 + type: boolean 1163 + secretName: 1164 + description: SecretName is the name of the TLS secret 1165 + type: string 1166 + type: object 1167 + type: object 1168 + nodeSelector: 1169 + additionalProperties: 1170 + type: string 1171 + description: NodeSelector for pod scheduling 1172 + type: object 1173 + openshift: 1174 + description: OpenShift contains OpenShift-specific configuration 1175 + properties: 1176 + route: 1177 + description: Route configures OpenShift Route creation 1178 + properties: 1179 + annotations: 1180 + additionalProperties: 1181 + type: string 1182 + description: Annotations to add to the Route 1183 + type: object 1184 + enabled: 1185 + description: Enabled enables Route creation 1186 + type: boolean 1187 + tls: 1188 + description: TLS configures TLS termination for the Route 1189 + properties: 1190 + caCertificate: 1191 + description: CACertificate is the PEM-encoded CA certificate 1192 + type: string 1193 + certificate: 1194 + description: Certificate is the PEM-encoded certificate 1195 + type: string 1196 + destinationCACertificate: 1197 + description: DestinationCACertificate is used for reencrypt 1198 + termination 1199 + type: string 1200 + insecureEdgeTerminationPolicy: 1201 + default: Redirect 1202 + description: InsecureEdgeTerminationPolicy specifies behavior 1203 + for insecure connections 1204 + enum: 1205 + - Allow 1206 + - Redirect 1207 + - None 1208 + type: string 1209 + key: 1210 + description: Key is the PEM-encoded private key 1211 + type: string 1212 + termination: 1213 + default: edge 1214 + description: Termination specifies the TLS termination 1215 + type (edge, passthrough, reencrypt) 1216 + enum: 1217 + - edge 1218 + - passthrough 1219 + - reencrypt 1220 + type: string 1221 + type: object 1222 + wildcardPolicy: 1223 + default: None 1224 + description: WildcardPolicy specifies the wildcard policy 1225 + (None, Subdomain) 1226 + type: string 1227 + type: object 1228 + scc: 1229 + description: SCC configures Security Context Constraints 1230 + properties: 1231 + allowHostIPC: 1232 + default: false 1233 + description: AllowHostIPC allows host IPC namespace 1234 + type: boolean 1235 + allowHostNetwork: 1236 + default: false 1237 + description: AllowHostNetwork allows host network access 1238 + type: boolean 1239 + allowHostPID: 1240 + default: false 1241 + description: AllowHostPID allows host PID namespace 1242 + type: boolean 1243 + allowHostPorts: 1244 + default: false 1245 + description: AllowHostPorts allows host port binding 1246 + type: boolean 1247 + allowPrivilegedContainer: 1248 + default: false 1249 + description: AllowPrivilegedContainer allows privileged containers 1250 + type: boolean 1251 + create: 1252 + description: Create specifies whether to create a custom SCC 1253 + type: boolean 1254 + fsGroup: 1255 + default: MustRunAs 1256 + description: FSGroup specifies the fs group strategy 1257 + type: string 1258 + name: 1259 + default: knot-scc 1260 + description: Name is the name of the SCC to use or create 1261 + type: string 1262 + readOnlyRootFilesystem: 1263 + default: false 1264 + description: ReadOnlyRootFilesystem requires read-only root 1265 + filesystem 1266 + type: boolean 1267 + runAsUser: 1268 + default: MustRunAsNonRoot 1269 + description: RunAsUser specifies the run as user strategy 1270 + type: string 1271 + seLinuxContext: 1272 + default: MustRunAs 1273 + description: SELinuxContext specifies the SELinux context 1274 + strategy 1275 + type: string 1276 + supplementalGroups: 1277 + default: RunAsAny 1278 + description: SupplementalGroups specifies the supplemental 1279 + groups strategy 1280 + type: string 1281 + volumes: 1282 + description: Volumes specifies allowed volume types 1283 + items: 1284 + type: string 1285 + type: array 1286 + type: object 1287 + type: object 1288 + owner: 1289 + description: Owner is the DID identifier of the server owner 1290 + type: string 1291 + replicas: 1292 + default: 1 1293 + description: Replicas is the number of Knot server instances to run 1294 + format: int32 1295 + minimum: 1 1296 + type: integer 1297 + resources: 1298 + description: Resources defines compute resource requirements 1299 + properties: 1300 + claims: 1301 + description: |- 1302 + Claims lists the names of resources, defined in spec.resourceClaims, 1303 + that are used by this container. 1304 + 1305 + This field depends on the 1306 + DynamicResourceAllocation feature gate. 1307 + 1308 + This field is immutable. It can only be set for containers. 1309 + items: 1310 + description: ResourceClaim references one entry in PodSpec.ResourceClaims. 1311 + properties: 1312 + name: 1313 + description: |- 1314 + Name must match the name of one entry in pod.spec.resourceClaims of 1315 + the Pod where this field is used. It makes that resource available 1316 + inside a container. 1317 + type: string 1318 + request: 1319 + description: |- 1320 + Request is the name chosen for a request in the referenced claim. 1321 + If empty, everything from the claim is made available, otherwise 1322 + only the result of this request. 1323 + type: string 1324 + required: 1325 + - name 1326 + type: object 1327 + type: array 1328 + x-kubernetes-list-map-keys: 1329 + - name 1330 + x-kubernetes-list-type: map 1331 + limits: 1332 + additionalProperties: 1333 + anyOf: 1334 + - type: integer 1335 + - type: string 1336 + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1337 + x-kubernetes-int-or-string: true 1338 + description: |- 1339 + Limits describes the maximum amount of compute resources allowed. 1340 + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 1341 + type: object 1342 + requests: 1343 + additionalProperties: 1344 + anyOf: 1345 + - type: integer 1346 + - type: string 1347 + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ 1348 + x-kubernetes-int-or-string: true 1349 + description: |- 1350 + Requests describes the minimum amount of compute resources required. 1351 + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, 1352 + otherwise to an implementation-defined value. Requests cannot exceed Limits. 1353 + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 1354 + type: object 1355 + type: object 1356 + serviceAccountName: 1357 + description: ServiceAccountName is the name of the ServiceAccount 1358 + to use 1359 + type: string 1360 + ssh: 1361 + description: SSH configures the SSH server for git operations 1362 + properties: 1363 + annotations: 1364 + additionalProperties: 1365 + type: string 1366 + description: Annotations to add to the SSH Service 1367 + type: object 1368 + enabled: 1369 + description: Enabled enables SSH access for git operations 1370 + type: boolean 1371 + loadBalancerIP: 1372 + description: LoadBalancerIP is the static IP for LoadBalancer 1373 + type services 1374 + type: string 1375 + nodePort: 1376 + description: NodePort is the port to use when ServiceType is NodePort 1377 + format: int32 1378 + type: integer 1379 + port: 1380 + default: 22 1381 + description: Port is the SSH port to expose 1382 + format: int32 1383 + type: integer 1384 + serviceType: 1385 + default: LoadBalancer 1386 + description: ServiceType is the Kubernetes Service type for SSH 1387 + type: string 1388 + type: object 1389 + storage: 1390 + description: Storage configures persistent storage for repositories 1391 + and database 1392 + properties: 1393 + dbPath: 1394 + default: /data/db 1395 + description: DBPath is the path where the database is stored 1396 + type: string 1397 + dbSize: 1398 + default: 1Gi 1399 + description: DBSize is the size of the database PVC 1400 + type: string 1401 + dbStorageClass: 1402 + description: DBStorageClass is the StorageClass for database storage 1403 + type: string 1404 + repoPath: 1405 + default: /data/repos 1406 + description: RepoPath is the path where repositories are stored 1407 + type: string 1408 + repoSize: 1409 + default: 10Gi 1410 + description: RepoSize is the size of the repository PVC 1411 + type: string 1412 + repoStorageClass: 1413 + description: RepoStorageClass is the StorageClass for repository 1414 + storage 1415 + type: string 1416 + type: object 1417 + tolerations: 1418 + description: Tolerations for pod scheduling 1419 + items: 1420 + description: |- 1421 + The pod this Toleration is attached to tolerates any taint that matches 1422 + the triple <key,value,effect> using the matching operator <operator>. 1423 + properties: 1424 + effect: 1425 + description: |- 1426 + Effect indicates the taint effect to match. Empty means match all taint effects. 1427 + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. 1428 + type: string 1429 + key: 1430 + description: |- 1431 + Key is the taint key that the toleration applies to. Empty means match all taint keys. 1432 + If the key is empty, operator must be Exists; this combination means to match all values and all keys. 1433 + type: string 1434 + operator: 1435 + description: |- 1436 + Operator represents a key's relationship to the value. 1437 + Valid operators are Exists, Equal, Lt, and Gt. Defaults to Equal. 1438 + Exists is equivalent to wildcard for value, so that a pod can 1439 + tolerate all taints of a particular category. 1440 + Lt and Gt perform numeric comparisons (requires feature gate TaintTolerationComparisonOperators). 1441 + type: string 1442 + tolerationSeconds: 1443 + description: |- 1444 + TolerationSeconds represents the period of time the toleration (which must be 1445 + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, 1446 + it is not set, which means tolerate the taint forever (do not evict). Zero and 1447 + negative values will be treated as 0 (evict immediately) by the system. 1448 + format: int64 1449 + type: integer 1450 + value: 1451 + description: |- 1452 + Value is the taint value the toleration matches to. 1453 + If the operator is Exists, the value should be empty, otherwise just a regular string. 1454 + type: string 1455 + type: object 1456 + type: array 1457 + required: 1458 + - hostname 1459 + - owner 1460 + type: object 1461 + status: 1462 + description: KnotStatus defines the observed state of Knot 1463 + properties: 1464 + availableReplicas: 1465 + description: AvailableReplicas is the number of available replicas 1466 + format: int32 1467 + type: integer 1468 + conditions: 1469 + description: Conditions represent the latest available observations 1470 + items: 1471 + description: Condition contains details for one aspect of the current 1472 + state of this API Resource. 1473 + properties: 1474 + lastTransitionTime: 1475 + description: |- 1476 + lastTransitionTime is the last time the condition transitioned from one status to another. 1477 + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. 1478 + format: date-time 1479 + type: string 1480 + message: 1481 + description: |- 1482 + message is a human readable message indicating details about the transition. 1483 + This may be an empty string. 1484 + maxLength: 32768 1485 + type: string 1486 + observedGeneration: 1487 + description: |- 1488 + observedGeneration represents the .metadata.generation that the condition was set based upon. 1489 + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date 1490 + with respect to the current state of the instance. 1491 + format: int64 1492 + minimum: 0 1493 + type: integer 1494 + reason: 1495 + description: |- 1496 + reason contains a programmatic identifier indicating the reason for the condition's last transition. 1497 + Producers of specific condition types may define expected values and meanings for this field, 1498 + and whether the values are considered a guaranteed API. 1499 + The value should be a CamelCase string. 1500 + This field may not be empty. 1501 + maxLength: 1024 1502 + minLength: 1 1503 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ 1504 + type: string 1505 + status: 1506 + description: status of the condition, one of True, False, Unknown. 1507 + enum: 1508 + - "True" 1509 + - "False" 1510 + - Unknown 1511 + type: string 1512 + type: 1513 + description: type of condition in CamelCase or in foo.example.com/CamelCase. 1514 + maxLength: 316 1515 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ 1516 + type: string 1517 + required: 1518 + - lastTransitionTime 1519 + - message 1520 + - reason 1521 + - status 1522 + - type 1523 + type: object 1524 + type: array 1525 + observedGeneration: 1526 + description: ObservedGeneration is the most recent generation observed 1527 + format: int64 1528 + type: integer 1529 + phase: 1530 + description: Phase represents the current phase of the Knot deployment 1531 + enum: 1532 + - Pending 1533 + - Running 1534 + - Failed 1535 + - Unknown 1536 + type: string 1537 + readyReplicas: 1538 + description: ReadyReplicas is the number of ready replicas 1539 + format: int32 1540 + type: integer 1541 + sshURL: 1542 + description: SSHURL is the SSH URL for git operations 1543 + type: string 1544 + url: 1545 + description: URL is the external URL of the Knot server 1546 + type: string 1547 + type: object 1548 + type: object 1549 + served: true 1550 + storage: true 1551 + subresources: 1552 + status: {}
+11
config/default/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + 4 + namespace: knot-operator-system 5 + 6 + namePrefix: knot-operator- 7 + 8 + resources: 9 + - ../crd 10 + - ../rbac 11 + - ../manager
+4
config/manager/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + resources: 4 + - manager.yaml
+67
config/manager/manager.yaml
··· 1 + apiVersion: v1 2 + kind: Namespace 3 + metadata: 4 + name: knot-operator-system 5 + labels: 6 + control-plane: controller-manager 7 + app.kubernetes.io/name: knot-operator 8 + app.kubernetes.io/managed-by: kustomize 9 + --- 10 + apiVersion: apps/v1 11 + kind: Deployment 12 + metadata: 13 + name: knot-operator-controller-manager 14 + namespace: knot-operator-system 15 + labels: 16 + control-plane: controller-manager 17 + app.kubernetes.io/name: knot-operator 18 + app.kubernetes.io/managed-by: kustomize 19 + spec: 20 + selector: 21 + matchLabels: 22 + control-plane: controller-manager 23 + replicas: 1 24 + template: 25 + metadata: 26 + labels: 27 + control-plane: controller-manager 28 + annotations: 29 + kubectl.kubernetes.io/default-container: manager 30 + spec: 31 + securityContext: 32 + runAsNonRoot: true 33 + seccompProfile: 34 + type: RuntimeDefault 35 + containers: 36 + - name: manager 37 + image: image-registry.openshift-image-registry.svc:5000/knot-operator-system/knot-operator:latest 38 + command: 39 + - /manager 40 + args: 41 + - --leader-elect 42 + securityContext: 43 + allowPrivilegeEscalation: false 44 + capabilities: 45 + drop: 46 + - ALL 47 + livenessProbe: 48 + httpGet: 49 + path: /healthz 50 + port: 8081 51 + initialDelaySeconds: 15 52 + periodSeconds: 20 53 + readinessProbe: 54 + httpGet: 55 + path: /readyz 56 + port: 8081 57 + initialDelaySeconds: 5 58 + periodSeconds: 10 59 + resources: 60 + limits: 61 + cpu: 500m 62 + memory: 128Mi 63 + requests: 64 + cpu: 10m 65 + memory: 64Mi 66 + serviceAccountName: knot-operator-controller-manager 67 + terminationGracePeriodSeconds: 10
+27
config/openshift/buildconfig.yaml
··· 1 + apiVersion: build.openshift.io/v1 2 + kind: BuildConfig 3 + metadata: 4 + name: knot-operator 5 + namespace: knot-operator-system 6 + spec: 7 + output: 8 + to: 9 + kind: ImageStreamTag 10 + name: knot-operator:latest 11 + source: 12 + type: Binary 13 + binary: {} 14 + strategy: 15 + type: Docker 16 + dockerStrategy: 17 + dockerfilePath: Dockerfile 18 + triggers: [] 19 + --- 20 + apiVersion: image.openshift.io/v1 21 + kind: ImageStream 22 + metadata: 23 + name: knot-operator 24 + namespace: knot-operator-system 25 + spec: 26 + lookupPolicy: 27 + local: true
+19
config/openshift/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + 4 + namespace: knot-operator-system 5 + 6 + resources: 7 + - ../crd 8 + - ../rbac 9 + - ../manager 10 + - buildconfig.yaml 11 + 12 + patches: 13 + - patch: |- 14 + - op: replace 15 + path: /spec/template/spec/containers/0/image 16 + value: image-registry.openshift-image-registry.svc:5000/knot-operator-system/knot-operator:latest 17 + target: 18 + kind: Deployment 19 + name: knot-operator-controller-manager
+6
config/rbac/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + resources: 4 + - service_account.yaml 5 + - role.yaml 6 + - role_binding.yaml
+97
config/rbac/role.yaml
··· 1 + --- 2 + apiVersion: rbac.authorization.k8s.io/v1 3 + kind: ClusterRole 4 + metadata: 5 + name: knot-operator 6 + rules: 7 + - apiGroups: 8 + - "" 9 + resources: 10 + - configmaps 11 + - persistentvolumeclaims 12 + - secrets 13 + - serviceaccounts 14 + - services 15 + verbs: 16 + - create 17 + - delete 18 + - get 19 + - list 20 + - patch 21 + - update 22 + - watch 23 + - apiGroups: 24 + - apps 25 + resources: 26 + - deployments 27 + verbs: 28 + - create 29 + - delete 30 + - get 31 + - list 32 + - patch 33 + - update 34 + - watch 35 + - apiGroups: 36 + - networking.k8s.io 37 + resources: 38 + - ingresses 39 + verbs: 40 + - create 41 + - delete 42 + - get 43 + - list 44 + - patch 45 + - update 46 + - watch 47 + - apiGroups: 48 + - route.openshift.io 49 + resources: 50 + - routes 51 + verbs: 52 + - create 53 + - delete 54 + - get 55 + - list 56 + - patch 57 + - update 58 + - watch 59 + - apiGroups: 60 + - security.openshift.io 61 + resources: 62 + - securitycontextconstraints 63 + verbs: 64 + - create 65 + - delete 66 + - get 67 + - list 68 + - patch 69 + - update 70 + - use 71 + - watch 72 + - apiGroups: 73 + - tangled.org 74 + resources: 75 + - knots 76 + verbs: 77 + - create 78 + - delete 79 + - get 80 + - list 81 + - patch 82 + - update 83 + - watch 84 + - apiGroups: 85 + - tangled.org 86 + resources: 87 + - knots/finalizers 88 + verbs: 89 + - update 90 + - apiGroups: 91 + - tangled.org 92 + resources: 93 + - knots/status 94 + verbs: 95 + - get 96 + - patch 97 + - update
+12
config/rbac/role_binding.yaml
··· 1 + apiVersion: rbac.authorization.k8s.io/v1 2 + kind: ClusterRoleBinding 3 + metadata: 4 + name: knot-operator-manager-rolebinding 5 + roleRef: 6 + apiGroup: rbac.authorization.k8s.io 7 + kind: ClusterRole 8 + name: knot-operator-manager-role 9 + subjects: 10 + - kind: ServiceAccount 11 + name: knot-operator-controller-manager 12 + namespace: knot-operator-system
+5
config/rbac/service_account.yaml
··· 1 + apiVersion: v1 2 + kind: ServiceAccount 3 + metadata: 4 + name: knot-operator-controller-manager 5 + namespace: knot-operator-system
+43
config/samples/knot_v1alpha1_knot.yaml
··· 1 + apiVersion: tangled.org/v1alpha1 2 + kind: Knot 3 + metadata: 4 + name: my-knot 5 + namespace: default 6 + spec: 7 + hostname: knot.example.com 8 + owner: did:plc:exampleuserid12345 9 + 10 + image: docker.io/tngl/knot:v1.10.0-alpha 11 + replicas: 1 12 + 13 + appviewEndpoint: https://tangled.org 14 + 15 + storage: 16 + repoSize: 20Gi 17 + dbSize: 2Gi 18 + repoPath: /data/repos 19 + dbPath: /data/db 20 + 21 + resources: 22 + requests: 23 + cpu: 100m 24 + memory: 256Mi 25 + limits: 26 + cpu: 500m 27 + memory: 512Mi 28 + 29 + ingress: 30 + enabled: true 31 + ingressClassName: nginx 32 + annotations: 33 + nginx.ingress.kubernetes.io/proxy-body-size: "100m" 34 + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" 35 + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" 36 + tls: 37 + enabled: true 38 + secretName: knot-tls 39 + 40 + ssh: 41 + enabled: true 42 + port: 22 43 + serviceType: LoadBalancer
+62
config/samples/knot_v1alpha1_knot_openshift.yaml
··· 1 + apiVersion: tangled.org/v1alpha1 2 + kind: Knot 3 + metadata: 4 + name: my-knot 5 + namespace: knot 6 + spec: 7 + hostname: knot.apps.example.com 8 + owner: did:plc:exampleuserid12345 9 + 10 + image: docker.io/tngl/knot:v1.10.0-alpha 11 + replicas: 1 12 + 13 + appviewEndpoint: https://tangled.org 14 + 15 + serviceAccountName: knot-sa 16 + 17 + storage: 18 + repoStorageClass: gp3-csi 19 + repoSize: 50Gi 20 + dbStorageClass: gp3-csi 21 + dbSize: 5Gi 22 + repoPath: /data/repos 23 + dbPath: /data/db 24 + 25 + resources: 26 + requests: 27 + cpu: 200m 28 + memory: 512Mi 29 + limits: 30 + cpu: "1" 31 + memory: 1Gi 32 + 33 + openshift: 34 + route: 35 + enabled: true 36 + annotations: 37 + haproxy.router.openshift.io/timeout: 3600s 38 + tls: 39 + termination: edge 40 + insecureEdgeTerminationPolicy: Redirect 41 + 42 + scc: 43 + create: true 44 + name: knot-scc 45 + runAsUser: MustRunAsNonRoot 46 + fsGroup: MustRunAs 47 + seLinuxContext: MustRunAs 48 + volumes: 49 + - configMap 50 + - downwardAPI 51 + - emptyDir 52 + - persistentVolumeClaim 53 + - projected 54 + - secret 55 + 56 + ssh: 57 + enabled: true 58 + port: 2222 59 + serviceType: NodePort 60 + nodePort: 30022 61 + annotations: 62 + service.beta.kubernetes.io/aws-load-balancer-type: nlb
+4
config/samples/kustomization.yaml
··· 1 + apiVersion: kustomize.config.k8s.io/v1beta1 2 + kind: Kustomization 3 + resources: 4 + - knot_v1alpha1_knot.yaml
+68
go.mod
··· 1 + module github.com/josie/knot-operator 2 + 3 + go 1.25.0 4 + 5 + require ( 6 + github.com/go-logr/logr v1.4.3 7 + github.com/stretchr/testify v1.11.1 8 + k8s.io/api v0.35.0 9 + k8s.io/apimachinery v0.35.0 10 + k8s.io/client-go v0.35.0 11 + sigs.k8s.io/controller-runtime v0.21.0 12 + ) 13 + 14 + require ( 15 + github.com/beorn7/perks v1.0.1 // indirect 16 + github.com/cespare/xxhash/v2 v2.3.0 // indirect 17 + github.com/davecgh/go-spew v1.1.1 // indirect 18 + github.com/emicklei/go-restful/v3 v3.12.2 // indirect 19 + github.com/evanphx/json-patch/v5 v5.9.11 // indirect 20 + github.com/fsnotify/fsnotify v1.7.0 // indirect 21 + github.com/fxamacker/cbor/v2 v2.9.0 // indirect 22 + github.com/go-logr/zapr v1.3.0 // indirect 23 + github.com/go-openapi/jsonpointer v0.21.0 // indirect 24 + github.com/go-openapi/jsonreference v0.20.2 // indirect 25 + github.com/go-openapi/swag v0.23.0 // indirect 26 + github.com/gogo/protobuf v1.3.2 // indirect 27 + github.com/google/btree v1.1.3 // indirect 28 + github.com/google/gnostic-models v0.7.0 // indirect 29 + github.com/google/go-cmp v0.7.0 // indirect 30 + github.com/google/uuid v1.6.0 // indirect 31 + github.com/josharian/intern v1.0.0 // indirect 32 + github.com/json-iterator/go v1.1.12 // indirect 33 + github.com/mailru/easyjson v0.7.7 // indirect 34 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 36 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 + github.com/pmezard/go-difflib v1.0.0 // indirect 38 + github.com/prometheus/client_golang v1.22.0 // indirect 39 + github.com/prometheus/client_model v0.6.1 // indirect 40 + github.com/prometheus/common v0.62.0 // indirect 41 + github.com/prometheus/procfs v0.15.1 // indirect 42 + github.com/spf13/pflag v1.0.9 // indirect 43 + github.com/x448/float16 v0.8.4 // indirect 44 + go.uber.org/multierr v1.11.0 // indirect 45 + go.uber.org/zap v1.27.0 // indirect 46 + go.yaml.in/yaml/v2 v2.4.3 // indirect 47 + go.yaml.in/yaml/v3 v3.0.4 // indirect 48 + golang.org/x/net v0.47.0 // indirect 49 + golang.org/x/oauth2 v0.30.0 // indirect 50 + golang.org/x/sync v0.18.0 // indirect 51 + golang.org/x/sys v0.38.0 // indirect 52 + golang.org/x/term v0.37.0 // indirect 53 + golang.org/x/text v0.31.0 // indirect 54 + golang.org/x/time v0.9.0 // indirect 55 + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 56 + google.golang.org/protobuf v1.36.8 // indirect 57 + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect 58 + gopkg.in/inf.v0 v0.9.1 // indirect 59 + gopkg.in/yaml.v3 v3.0.1 // indirect 60 + k8s.io/apiextensions-apiserver v0.33.0 // indirect 61 + k8s.io/klog/v2 v2.130.1 // indirect 62 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect 63 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect 64 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 65 + sigs.k8s.io/randfill v1.0.0 // indirect 66 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 67 + sigs.k8s.io/yaml v1.6.0 // indirect 68 + )
+204
go.sum
··· 1 + github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 2 + github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 3 + github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 + github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 5 + github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= 6 + github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 7 + github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 8 + github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 9 + github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 + github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 14 + github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 15 + github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= 16 + github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= 17 + github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 18 + github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 19 + github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 20 + github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 21 + github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 22 + github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 23 + github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 24 + github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 25 + github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 26 + github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= 27 + github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 28 + github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 29 + github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 30 + github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 31 + github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 32 + github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 33 + github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 34 + github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 35 + github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 36 + github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 37 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 38 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 39 + github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= 40 + github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 41 + github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 42 + github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 43 + github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 44 + github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 45 + github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 46 + github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 47 + github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 48 + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= 49 + github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 50 + github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 + github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 + github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 53 + github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 54 + github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 55 + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 56 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 57 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 58 + github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 59 + github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 60 + github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 61 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 62 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 63 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 64 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 65 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 66 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 67 + github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 68 + github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 69 + github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 70 + github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 71 + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 73 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 74 + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 75 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 76 + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 77 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 78 + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 79 + github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= 80 + github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= 81 + github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= 82 + github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 83 + github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 84 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 85 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 86 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 + github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 88 + github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 89 + github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 90 + github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 91 + github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 92 + github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 93 + github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 94 + github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 95 + github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 96 + github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 97 + github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 98 + github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 99 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 100 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 101 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 102 + github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 103 + github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 104 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 105 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 106 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 107 + github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 108 + github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 109 + github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 110 + github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 111 + github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 112 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 113 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 114 + go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 115 + go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 116 + go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 117 + go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 118 + go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 119 + go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 120 + go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= 121 + go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= 122 + go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 123 + go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 124 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 125 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 126 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 127 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 128 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 129 + golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= 130 + golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= 131 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 132 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 133 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 134 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 135 + golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 136 + golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 137 + golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 138 + golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 139 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 140 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 141 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 142 + golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= 143 + golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 144 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 145 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 + golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 148 + golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 149 + golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= 150 + golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= 151 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 152 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 153 + golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 154 + golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 155 + golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 156 + golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 157 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 158 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 159 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 160 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 161 + golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= 162 + golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= 163 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 + gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 168 + gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 169 + google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 170 + google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 171 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 173 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 174 + gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 175 + gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 176 + gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 177 + gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 178 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 180 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 181 + k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= 182 + k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= 183 + k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= 184 + k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= 185 + k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= 186 + k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= 187 + k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= 188 + k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= 189 + k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 190 + k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 191 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= 192 + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= 193 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 194 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 195 + sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= 196 + sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= 197 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= 198 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 199 + sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 200 + sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 201 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 202 + sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 203 + sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 204 + sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+739
internal/controller/knot_controller.go
··· 1 + package controller 2 + 3 + import ( 4 + "context" 5 + "crypto/rand" 6 + "encoding/hex" 7 + "fmt" 8 + "time" 9 + 10 + "github.com/go-logr/logr" 11 + appsv1 "k8s.io/api/apps/v1" 12 + corev1 "k8s.io/api/core/v1" 13 + networkingv1 "k8s.io/api/networking/v1" 14 + "k8s.io/apimachinery/pkg/api/errors" 15 + "k8s.io/apimachinery/pkg/api/meta" 16 + "k8s.io/apimachinery/pkg/api/resource" 17 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 + "k8s.io/apimachinery/pkg/runtime" 19 + "k8s.io/apimachinery/pkg/types" 20 + "k8s.io/apimachinery/pkg/util/intstr" 21 + ctrl "sigs.k8s.io/controller-runtime" 22 + "sigs.k8s.io/controller-runtime/pkg/client" 23 + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 24 + "sigs.k8s.io/controller-runtime/pkg/log" 25 + 26 + tangledv1alpha1 "github.com/josie/knot-operator/api/v1alpha1" 27 + ) 28 + 29 + const ( 30 + knotFinalizer = "tangled.org/finalizer" 31 + typeAvailable = "Available" 32 + typeProgressing = "Progressing" 33 + typeDegraded = "Degraded" 34 + defaultRequeueTime = 30 * time.Second 35 + 36 + knotHTTPPort = 5555 37 + knotInternalPort = 5444 38 + sshPort = 22 39 + ) 40 + 41 + func (r *KnotReconciler) loggerFor(ctx context.Context, knot *tangledv1alpha1.Knot) logr.Logger { 42 + return log.FromContext(ctx).WithValues( 43 + "knot", knot.Name, 44 + "namespace", knot.Namespace, 45 + ) 46 + } 47 + 48 + type KnotReconciler struct { 49 + client.Client 50 + Scheme *runtime.Scheme 51 + IsOpenShift bool 52 + } 53 + 54 + // +kubebuilder:rbac:groups=tangled.org,resources=knots,verbs=get;list;watch;create;update;patch;delete 55 + // +kubebuilder:rbac:groups=tangled.org,resources=knots/status,verbs=get;update;patch 56 + // +kubebuilder:rbac:groups=tangled.org,resources=knots/finalizers,verbs=update 57 + // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete 58 + // +kubebuilder:rbac:groups=core,resources=services;persistentvolumeclaims;configmaps;secrets;serviceaccounts,verbs=get;list;watch;create;update;patch;delete 59 + // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete 60 + // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch;create;update;patch;delete 61 + // +kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,verbs=get;list;watch;create;update;patch;delete;use 62 + 63 + func (r *KnotReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 64 + baseLogger := log.FromContext(ctx) 65 + 66 + knot := &tangledv1alpha1.Knot{} 67 + if err := r.Get(ctx, req.NamespacedName, knot); err != nil { 68 + if errors.IsNotFound(err) { 69 + baseLogger.Info("knot resource not found", "name", req.Name, "namespace", req.Namespace) 70 + return ctrl.Result{}, nil 71 + } 72 + baseLogger.Error(err, "failed to get knot resource", "name", req.Name, "namespace", req.Namespace) 73 + return ctrl.Result{}, err 74 + } 75 + 76 + logger := r.loggerFor(ctx, knot) 77 + 78 + if knot.Status.Conditions == nil { 79 + knot.Status.Conditions = []metav1.Condition{} 80 + } 81 + 82 + if !knot.DeletionTimestamp.IsZero() { 83 + return r.handleDeletion(ctx, knot, logger) 84 + } 85 + 86 + if !controllerutil.ContainsFinalizer(knot, knotFinalizer) { 87 + logger.V(1).Info("adding finalizer") 88 + controllerutil.AddFinalizer(knot, knotFinalizer) 89 + if err := r.Update(ctx, knot); err != nil { 90 + return ctrl.Result{}, err 91 + } 92 + } 93 + 94 + r.setDefaults(knot) 95 + 96 + if err := r.reconcilePVCs(ctx, knot); err != nil { 97 + return r.handleError(ctx, knot, err, "Failed to reconcile PVCs") 98 + } 99 + 100 + if err := r.reconcileConfigMap(ctx, knot); err != nil { 101 + return r.handleError(ctx, knot, err, "Failed to reconcile ConfigMap") 102 + } 103 + 104 + if err := r.reconcileSecret(ctx, knot); err != nil { 105 + return r.handleError(ctx, knot, err, "Failed to reconcile Secret") 106 + } 107 + 108 + if err := r.reconcileService(ctx, knot); err != nil { 109 + return r.handleError(ctx, knot, err, "Failed to reconcile Service") 110 + } 111 + 112 + if knot.Spec.SSH != nil && knot.Spec.SSH.Enabled { 113 + if err := r.reconcileSSHService(ctx, knot); err != nil { 114 + return r.handleError(ctx, knot, err, "Failed to reconcile SSH Service") 115 + } 116 + } 117 + 118 + if err := r.reconcileDeployment(ctx, knot); err != nil { 119 + return r.handleError(ctx, knot, err, "Failed to reconcile Deployment") 120 + } 121 + 122 + if knot.Spec.Ingress != nil && knot.Spec.Ingress.Enabled { 123 + if err := r.reconcileIngress(ctx, knot); err != nil { 124 + return r.handleError(ctx, knot, err, "Failed to reconcile Ingress") 125 + } 126 + } 127 + 128 + if r.IsOpenShift && knot.Spec.OpenShift != nil { 129 + if knot.Spec.OpenShift.Route != nil && knot.Spec.OpenShift.Route.Enabled { 130 + if err := r.reconcileRoute(ctx, knot); err != nil { 131 + return r.handleError(ctx, knot, err, "Failed to reconcile Route") 132 + } 133 + } 134 + if knot.Spec.OpenShift.SCC != nil && knot.Spec.OpenShift.SCC.Create { 135 + if err := r.reconcileSCC(ctx, knot); err != nil { 136 + return r.handleError(ctx, knot, err, "Failed to reconcile SCC") 137 + } 138 + } 139 + } 140 + 141 + return r.updateStatus(ctx, knot) 142 + } 143 + 144 + func (r *KnotReconciler) setDefaults(knot *tangledv1alpha1.Knot) { 145 + if knot.Spec.Image == "" { 146 + knot.Spec.Image = "docker.io/tngl/knot:v1.10.0-alpha" 147 + } 148 + if knot.Spec.ImagePullPolicy == "" { 149 + knot.Spec.ImagePullPolicy = corev1.PullIfNotPresent 150 + } 151 + if knot.Spec.Replicas == 0 { 152 + knot.Spec.Replicas = 1 153 + } 154 + if knot.Spec.AppviewEndpoint == "" { 155 + knot.Spec.AppviewEndpoint = "https://tangled.org" 156 + } 157 + if knot.Spec.Storage.RepoSize == "" { 158 + knot.Spec.Storage.RepoSize = "10Gi" 159 + } 160 + if knot.Spec.Storage.DBSize == "" { 161 + knot.Spec.Storage.DBSize = "1Gi" 162 + } 163 + if knot.Spec.Storage.RepoPath == "" { 164 + knot.Spec.Storage.RepoPath = "/data/repos" 165 + } 166 + if knot.Spec.Storage.DBPath == "" { 167 + knot.Spec.Storage.DBPath = "/data/db" 168 + } 169 + } 170 + 171 + func (r *KnotReconciler) handleDeletion(ctx context.Context, knot *tangledv1alpha1.Knot, logger logr.Logger) (ctrl.Result, error) { 172 + if controllerutil.ContainsFinalizer(knot, knotFinalizer) { 173 + logger.Info("removing finalizer for deletion") 174 + controllerutil.RemoveFinalizer(knot, knotFinalizer) 175 + if err := r.Update(ctx, knot); err != nil { 176 + return ctrl.Result{}, err 177 + } 178 + } 179 + 180 + return ctrl.Result{}, nil 181 + } 182 + 183 + func (r *KnotReconciler) handleError(ctx context.Context, knot *tangledv1alpha1.Knot, err error, msg string) (ctrl.Result, error) { 184 + logger := r.loggerFor(ctx, knot) 185 + logger.Error(err, msg) 186 + 187 + meta.SetStatusCondition(&knot.Status.Conditions, metav1.Condition{ 188 + Type: typeDegraded, 189 + Status: metav1.ConditionTrue, 190 + Reason: "ReconciliationFailed", 191 + Message: fmt.Sprintf("%s: %v", msg, err), 192 + LastTransitionTime: metav1.Now(), 193 + }) 194 + 195 + knot.Status.Phase = "Failed" 196 + if statusErr := r.Status().Update(ctx, knot); statusErr != nil { 197 + logger.Error(statusErr, "failed to update status") 198 + } 199 + 200 + return ctrl.Result{RequeueAfter: defaultRequeueTime}, err 201 + } 202 + 203 + func (r *KnotReconciler) reconcilePVCs(ctx context.Context, knot *tangledv1alpha1.Knot) error { 204 + repoPVC := r.buildPVC(knot, "repos", knot.Spec.Storage.RepoSize, knot.Spec.Storage.RepoStorageClass) 205 + if err := r.createOrUpdate(ctx, repoPVC, knot, func(existing, desired client.Object) {}); err != nil { 206 + return err 207 + } 208 + 209 + dbPVC := r.buildPVC(knot, "db", knot.Spec.Storage.DBSize, knot.Spec.Storage.DBStorageClass) 210 + return r.createOrUpdate(ctx, dbPVC, knot, func(existing, desired client.Object) {}) 211 + } 212 + 213 + func (r *KnotReconciler) buildPVC(knot *tangledv1alpha1.Knot, suffix, size, storageClass string) *corev1.PersistentVolumeClaim { 214 + pvc := &corev1.PersistentVolumeClaim{ 215 + ObjectMeta: metav1.ObjectMeta{ 216 + Name: fmt.Sprintf("%s-%s", knot.Name, suffix), 217 + Namespace: knot.Namespace, 218 + Labels: r.labelsForKnot(knot), 219 + }, 220 + Spec: corev1.PersistentVolumeClaimSpec{ 221 + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, 222 + Resources: corev1.VolumeResourceRequirements{ 223 + Requests: corev1.ResourceList{ 224 + corev1.ResourceStorage: resource.MustParse(size), 225 + }, 226 + }, 227 + }, 228 + } 229 + 230 + if storageClass != "" { 231 + pvc.Spec.StorageClassName = &storageClass 232 + } 233 + 234 + return pvc 235 + } 236 + 237 + type updateFunc func(existing, desired client.Object) 238 + 239 + func (r *KnotReconciler) createOrUpdate(ctx context.Context, obj client.Object, knot *tangledv1alpha1.Knot, update updateFunc) error { 240 + if err := controllerutil.SetControllerReference(knot, obj, r.Scheme); err != nil { 241 + return err 242 + } 243 + 244 + existing := obj.DeepCopyObject().(client.Object) 245 + err := r.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, existing) 246 + if err != nil { 247 + if errors.IsNotFound(err) { 248 + return r.Create(ctx, obj) 249 + } 250 + return err 251 + } 252 + 253 + if update != nil { 254 + update(existing, obj) 255 + } 256 + return r.Update(ctx, existing) 257 + } 258 + 259 + func (r *KnotReconciler) reconcileConfigMap(ctx context.Context, knot *tangledv1alpha1.Knot) error { 260 + cm := &corev1.ConfigMap{ 261 + ObjectMeta: metav1.ObjectMeta{ 262 + Name: fmt.Sprintf("%s-config", knot.Name), 263 + Namespace: knot.Namespace, 264 + Labels: r.labelsForKnot(knot), 265 + }, 266 + Data: map[string]string{ 267 + "KNOT_SERVER_HOSTNAME": knot.Spec.Hostname, 268 + "KNOT_SERVER_OWNER": knot.Spec.Owner, 269 + "APPVIEW_ENDPOINT": knot.Spec.AppviewEndpoint, 270 + "KNOT_REPO_SCAN_PATH": knot.Spec.Storage.RepoPath, 271 + "KNOT_SERVER_DB_PATH": fmt.Sprintf("%s/knotserver.db", knot.Spec.Storage.DBPath), 272 + "KNOT_SERVER_INTERNAL_LISTEN_ADDR": fmt.Sprintf("0.0.0.0:%d", knotInternalPort), 273 + "KNOT_SERVER_LISTEN_ADDR": fmt.Sprintf("0.0.0.0:%d", knotHTTPPort), 274 + }, 275 + } 276 + 277 + return r.createOrUpdate(ctx, cm, knot, func(existing, desired client.Object) { 278 + existing.(*corev1.ConfigMap).Data = desired.(*corev1.ConfigMap).Data 279 + }) 280 + } 281 + 282 + func (r *KnotReconciler) reconcileSecret(ctx context.Context, knot *tangledv1alpha1.Knot) error { 283 + secretName := fmt.Sprintf("%s-secret", knot.Name) 284 + 285 + existing := &corev1.Secret{} 286 + err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: knot.Namespace}, existing) 287 + if err == nil { 288 + if val, ok := existing.Data["KNOT_SERVER_SECRET"]; ok && len(val) > 0 { 289 + return nil 290 + } 291 + logger := r.loggerFor(ctx, knot) 292 + logger.Info("regenerating invalid secret", "secret", secretName) 293 + if err := r.Delete(ctx, existing); err != nil { 294 + return fmt.Errorf("failed to delete corrupted secret: %w", err) 295 + } 296 + } else if !errors.IsNotFound(err) { 297 + return err 298 + } 299 + 300 + serverSecret, err := generateRandomHex(32) 301 + if err != nil { 302 + return fmt.Errorf("failed to generate server secret: %w", err) 303 + } 304 + 305 + secret := &corev1.Secret{ 306 + ObjectMeta: metav1.ObjectMeta{ 307 + Name: secretName, 308 + Namespace: knot.Namespace, 309 + Labels: r.labelsForKnot(knot), 310 + }, 311 + Type: corev1.SecretTypeOpaque, 312 + StringData: map[string]string{ 313 + "KNOT_SERVER_SECRET": serverSecret, 314 + }, 315 + } 316 + 317 + if err := controllerutil.SetControllerReference(knot, secret, r.Scheme); err != nil { 318 + return err 319 + } 320 + 321 + return r.Create(ctx, secret) 322 + } 323 + 324 + func generateRandomHex(n int) (string, error) { 325 + bytes := make([]byte, n) 326 + if _, err := rand.Read(bytes); err != nil { 327 + return "", err 328 + } 329 + return hex.EncodeToString(bytes), nil 330 + } 331 + 332 + func (r *KnotReconciler) reconcileService(ctx context.Context, knot *tangledv1alpha1.Knot) error { 333 + svc := &corev1.Service{ 334 + ObjectMeta: metav1.ObjectMeta{ 335 + Name: knot.Name, 336 + Namespace: knot.Namespace, 337 + Labels: r.labelsForKnot(knot), 338 + }, 339 + Spec: corev1.ServiceSpec{ 340 + Selector: r.selectorLabelsForKnot(knot), 341 + Ports: []corev1.ServicePort{ 342 + { 343 + Name: "http", 344 + Port: knotHTTPPort, 345 + TargetPort: intstr.FromInt(knotHTTPPort), 346 + Protocol: corev1.ProtocolTCP, 347 + }, 348 + { 349 + Name: "internal", 350 + Port: knotInternalPort, 351 + TargetPort: intstr.FromInt(knotInternalPort), 352 + Protocol: corev1.ProtocolTCP, 353 + }, 354 + }, 355 + Type: corev1.ServiceTypeClusterIP, 356 + }, 357 + } 358 + 359 + return r.createOrUpdate(ctx, svc, knot, func(existing, desired client.Object) { 360 + existingSvc := existing.(*corev1.Service) 361 + desiredSvc := desired.(*corev1.Service) 362 + existingSvc.Spec.Ports = desiredSvc.Spec.Ports 363 + existingSvc.Spec.Selector = desiredSvc.Spec.Selector 364 + }) 365 + } 366 + 367 + func (r *KnotReconciler) reconcileSSHService(ctx context.Context, knot *tangledv1alpha1.Knot) error { 368 + sshSpec := knot.Spec.SSH 369 + if sshSpec == nil { 370 + return nil 371 + } 372 + 373 + port := sshSpec.Port 374 + if port == 0 { 375 + port = 22 376 + } 377 + 378 + serviceType := sshSpec.ServiceType 379 + if serviceType == "" { 380 + serviceType = corev1.ServiceTypeLoadBalancer 381 + } 382 + 383 + svc := &corev1.Service{ 384 + ObjectMeta: metav1.ObjectMeta{ 385 + Name: fmt.Sprintf("%s-ssh", knot.Name), 386 + Namespace: knot.Namespace, 387 + Labels: r.labelsForKnot(knot), 388 + Annotations: sshSpec.Annotations, 389 + }, 390 + Spec: corev1.ServiceSpec{ 391 + Selector: r.selectorLabelsForKnot(knot), 392 + Ports: []corev1.ServicePort{ 393 + { 394 + Name: "ssh", 395 + Port: port, 396 + TargetPort: intstr.FromInt(sshPort), 397 + Protocol: corev1.ProtocolTCP, 398 + }, 399 + }, 400 + Type: serviceType, 401 + }, 402 + } 403 + 404 + if sshSpec.LoadBalancerIP != "" && serviceType == corev1.ServiceTypeLoadBalancer { 405 + svc.Spec.LoadBalancerIP = sshSpec.LoadBalancerIP 406 + } 407 + 408 + if sshSpec.NodePort != 0 && serviceType == corev1.ServiceTypeNodePort { 409 + svc.Spec.Ports[0].NodePort = sshSpec.NodePort 410 + } 411 + 412 + return r.createOrUpdate(ctx, svc, knot, func(existing, desired client.Object) { 413 + existingSvc := existing.(*corev1.Service) 414 + desiredSvc := desired.(*corev1.Service) 415 + existingSvc.Spec.Ports = desiredSvc.Spec.Ports 416 + existingSvc.Spec.Type = desiredSvc.Spec.Type 417 + }) 418 + } 419 + 420 + func (r *KnotReconciler) reconcileDeployment(ctx context.Context, knot *tangledv1alpha1.Knot) error { 421 + dep := r.buildDeployment(knot) 422 + 423 + return r.createOrUpdate(ctx, dep, knot, func(existing, desired client.Object) { 424 + existing.(*appsv1.Deployment).Spec = desired.(*appsv1.Deployment).Spec 425 + }) 426 + } 427 + 428 + func (r *KnotReconciler) buildDeployment(knot *tangledv1alpha1.Knot) *appsv1.Deployment { 429 + labels := r.labelsForKnot(knot) 430 + selectorLabels := r.selectorLabelsForKnot(knot) 431 + 432 + envFrom := []corev1.EnvFromSource{ 433 + { 434 + ConfigMapRef: &corev1.ConfigMapEnvSource{ 435 + LocalObjectReference: corev1.LocalObjectReference{ 436 + Name: fmt.Sprintf("%s-config", knot.Name), 437 + }, 438 + }, 439 + }, 440 + { 441 + SecretRef: &corev1.SecretEnvSource{ 442 + LocalObjectReference: corev1.LocalObjectReference{ 443 + Name: fmt.Sprintf("%s-secret", knot.Name), 444 + }, 445 + Optional: func() *bool { b := true; return &b }(), 446 + }, 447 + }, 448 + } 449 + 450 + volumes := []corev1.Volume{ 451 + { 452 + Name: "repos", 453 + VolumeSource: corev1.VolumeSource{ 454 + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 455 + ClaimName: fmt.Sprintf("%s-repos", knot.Name), 456 + }, 457 + }, 458 + }, 459 + { 460 + Name: "db", 461 + VolumeSource: corev1.VolumeSource{ 462 + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 463 + ClaimName: fmt.Sprintf("%s-db", knot.Name), 464 + }, 465 + }, 466 + }, 467 + { 468 + Name: "home-git", 469 + VolumeSource: corev1.VolumeSource{ 470 + EmptyDir: &corev1.EmptyDirVolumeSource{}, 471 + }, 472 + }, 473 + } 474 + 475 + volumeMounts := []corev1.VolumeMount{ 476 + { 477 + Name: "repos", 478 + MountPath: knot.Spec.Storage.RepoPath, 479 + }, 480 + { 481 + Name: "db", 482 + MountPath: knot.Spec.Storage.DBPath, 483 + }, 484 + { 485 + Name: "home-git", 486 + MountPath: "/home/git", 487 + }, 488 + } 489 + 490 + container := corev1.Container{ 491 + Name: "knot", 492 + Image: knot.Spec.Image, 493 + ImagePullPolicy: knot.Spec.ImagePullPolicy, 494 + Ports: []corev1.ContainerPort{ 495 + { 496 + Name: "http", 497 + ContainerPort: knotHTTPPort, 498 + Protocol: corev1.ProtocolTCP, 499 + }, 500 + { 501 + Name: "internal", 502 + ContainerPort: knotInternalPort, 503 + Protocol: corev1.ProtocolTCP, 504 + }, 505 + }, 506 + EnvFrom: envFrom, 507 + Env: knot.Spec.ExtraEnv, 508 + VolumeMounts: volumeMounts, 509 + Resources: knot.Spec.Resources, 510 + LivenessProbe: &corev1.Probe{ 511 + ProbeHandler: corev1.ProbeHandler{ 512 + HTTPGet: &corev1.HTTPGetAction{ 513 + Path: "/", 514 + Port: intstr.FromInt(knotHTTPPort), 515 + }, 516 + }, 517 + InitialDelaySeconds: 10, 518 + PeriodSeconds: 10, 519 + }, 520 + ReadinessProbe: &corev1.Probe{ 521 + ProbeHandler: corev1.ProbeHandler{ 522 + HTTPGet: &corev1.HTTPGetAction{ 523 + Path: "/", 524 + Port: intstr.FromInt(knotHTTPPort), 525 + }, 526 + }, 527 + InitialDelaySeconds: 5, 528 + PeriodSeconds: 5, 529 + }, 530 + } 531 + 532 + if knot.Spec.SSH != nil && knot.Spec.SSH.Enabled { 533 + container.Ports = append(container.Ports, corev1.ContainerPort{ 534 + Name: "ssh", 535 + ContainerPort: sshPort, 536 + Protocol: corev1.ProtocolTCP, 537 + }) 538 + container.SecurityContext = &corev1.SecurityContext{ 539 + Capabilities: &corev1.Capabilities{ 540 + Add: []corev1.Capability{"SYS_CHROOT"}, 541 + }, 542 + } 543 + } 544 + 545 + dep := &appsv1.Deployment{ 546 + ObjectMeta: metav1.ObjectMeta{ 547 + Name: knot.Name, 548 + Namespace: knot.Namespace, 549 + Labels: labels, 550 + }, 551 + Spec: appsv1.DeploymentSpec{ 552 + Replicas: &knot.Spec.Replicas, 553 + Selector: &metav1.LabelSelector{ 554 + MatchLabels: selectorLabels, 555 + }, 556 + Template: corev1.PodTemplateSpec{ 557 + ObjectMeta: metav1.ObjectMeta{ 558 + Labels: labels, 559 + }, 560 + Spec: corev1.PodSpec{ 561 + ServiceAccountName: knot.Spec.ServiceAccountName, 562 + InitContainers: []corev1.Container{ 563 + { 564 + Name: "setup-repos-symlink", 565 + Image: "busybox:latest", 566 + Command: []string{ 567 + "sh", "-c", 568 + fmt.Sprintf("mkdir -p /home/git && rm -rf /home/git/repositories && ln -sf %s /home/git/repositories", knot.Spec.Storage.RepoPath), 569 + }, 570 + VolumeMounts: []corev1.VolumeMount{ 571 + { 572 + Name: "home-git", 573 + MountPath: "/home/git", 574 + }, 575 + }, 576 + }, 577 + }, 578 + Containers: []corev1.Container{container}, 579 + Volumes: volumes, 580 + NodeSelector: knot.Spec.NodeSelector, 581 + Tolerations: knot.Spec.Tolerations, 582 + Affinity: knot.Spec.Affinity, 583 + }, 584 + }, 585 + }, 586 + } 587 + 588 + return dep 589 + } 590 + 591 + func (r *KnotReconciler) reconcileIngress(ctx context.Context, knot *tangledv1alpha1.Knot) error { 592 + ingressSpec := knot.Spec.Ingress 593 + pathType := networkingv1.PathTypePrefix 594 + 595 + ingress := &networkingv1.Ingress{ 596 + ObjectMeta: metav1.ObjectMeta{ 597 + Name: knot.Name, 598 + Namespace: knot.Namespace, 599 + Labels: r.labelsForKnot(knot), 600 + Annotations: ingressSpec.Annotations, 601 + }, 602 + Spec: networkingv1.IngressSpec{ 603 + Rules: []networkingv1.IngressRule{ 604 + { 605 + Host: knot.Spec.Hostname, 606 + IngressRuleValue: networkingv1.IngressRuleValue{ 607 + HTTP: &networkingv1.HTTPIngressRuleValue{ 608 + Paths: []networkingv1.HTTPIngressPath{ 609 + { 610 + Path: "/", 611 + PathType: &pathType, 612 + Backend: networkingv1.IngressBackend{ 613 + Service: &networkingv1.IngressServiceBackend{ 614 + Name: knot.Name, 615 + Port: networkingv1.ServiceBackendPort{ 616 + Number: knotHTTPPort, 617 + }, 618 + }, 619 + }, 620 + }, 621 + }, 622 + }, 623 + }, 624 + }, 625 + }, 626 + }, 627 + } 628 + 629 + if ingressSpec.IngressClassName != "" { 630 + ingress.Spec.IngressClassName = &ingressSpec.IngressClassName 631 + } 632 + 633 + if ingressSpec.TLS != nil && ingressSpec.TLS.Enabled { 634 + ingress.Spec.TLS = []networkingv1.IngressTLS{ 635 + { 636 + Hosts: []string{knot.Spec.Hostname}, 637 + SecretName: ingressSpec.TLS.SecretName, 638 + }, 639 + } 640 + } 641 + 642 + return r.createOrUpdate(ctx, ingress, knot, func(existing, desired client.Object) { 643 + existingIngress := existing.(*networkingv1.Ingress) 644 + desiredIngress := desired.(*networkingv1.Ingress) 645 + existingIngress.Spec = desiredIngress.Spec 646 + existingIngress.Annotations = desiredIngress.Annotations 647 + }) 648 + } 649 + 650 + func (r *KnotReconciler) updateStatus(ctx context.Context, knot *tangledv1alpha1.Knot) (ctrl.Result, error) { 651 + logger := r.loggerFor(ctx, knot) 652 + 653 + dep := &appsv1.Deployment{} 654 + if err := r.Get(ctx, types.NamespacedName{Name: knot.Name, Namespace: knot.Namespace}, dep); err != nil { 655 + if !errors.IsNotFound(err) { 656 + return ctrl.Result{}, err 657 + } 658 + } else { 659 + knot.Status.ReadyReplicas = dep.Status.ReadyReplicas 660 + knot.Status.AvailableReplicas = dep.Status.AvailableReplicas 661 + } 662 + 663 + scheme := "http" 664 + if knot.Spec.Ingress != nil && knot.Spec.Ingress.TLS != nil && knot.Spec.Ingress.TLS.Enabled { 665 + scheme = "https" 666 + } 667 + if r.IsOpenShift && knot.Spec.OpenShift != nil && knot.Spec.OpenShift.Route != nil && knot.Spec.OpenShift.Route.TLS != nil { 668 + scheme = "https" 669 + } 670 + knot.Status.URL = fmt.Sprintf("%s://%s", scheme, knot.Spec.Hostname) 671 + 672 + if knot.Spec.SSH != nil && knot.Spec.SSH.Enabled { 673 + knot.Status.SSHURL = fmt.Sprintf("git@%s", knot.Spec.Hostname) 674 + } 675 + 676 + if knot.Status.ReadyReplicas == knot.Spec.Replicas { 677 + knot.Status.Phase = "Running" 678 + meta.SetStatusCondition(&knot.Status.Conditions, metav1.Condition{ 679 + Type: typeAvailable, 680 + Status: metav1.ConditionTrue, 681 + Reason: "DeploymentReady", 682 + Message: "Knot deployment is ready", 683 + LastTransitionTime: metav1.Now(), 684 + }) 685 + meta.SetStatusCondition(&knot.Status.Conditions, metav1.Condition{ 686 + Type: typeDegraded, 687 + Status: metav1.ConditionFalse, 688 + Reason: "DeploymentReady", 689 + Message: "Knot deployment is healthy", 690 + LastTransitionTime: metav1.Now(), 691 + }) 692 + } else { 693 + knot.Status.Phase = "Pending" 694 + meta.SetStatusCondition(&knot.Status.Conditions, metav1.Condition{ 695 + Type: typeProgressing, 696 + Status: metav1.ConditionTrue, 697 + Reason: "DeploymentProgressing", 698 + Message: fmt.Sprintf("Waiting for replicas: %d/%d ready", knot.Status.ReadyReplicas, knot.Spec.Replicas), 699 + LastTransitionTime: metav1.Now(), 700 + }) 701 + } 702 + 703 + knot.Status.ObservedGeneration = knot.Generation 704 + 705 + if err := r.Status().Update(ctx, knot); err != nil { 706 + logger.Error(err, "Failed to update Knot status") 707 + return ctrl.Result{}, err 708 + } 709 + 710 + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil 711 + } 712 + 713 + func (r *KnotReconciler) labelsForKnot(knot *tangledv1alpha1.Knot) map[string]string { 714 + return map[string]string{ 715 + "app.kubernetes.io/name": "knot", 716 + "app.kubernetes.io/instance": knot.Name, 717 + "app.kubernetes.io/managed-by": "knot-operator", 718 + "app.kubernetes.io/component": "server", 719 + } 720 + } 721 + 722 + func (r *KnotReconciler) selectorLabelsForKnot(knot *tangledv1alpha1.Knot) map[string]string { 723 + return map[string]string{ 724 + "app.kubernetes.io/name": "knot", 725 + "app.kubernetes.io/instance": knot.Name, 726 + } 727 + } 728 + 729 + func (r *KnotReconciler) SetupWithManager(mgr ctrl.Manager) error { 730 + return ctrl.NewControllerManagedBy(mgr). 731 + For(&tangledv1alpha1.Knot{}). 732 + Owns(&appsv1.Deployment{}). 733 + Owns(&corev1.Service{}). 734 + Owns(&corev1.PersistentVolumeClaim{}). 735 + Owns(&corev1.ConfigMap{}). 736 + Owns(&corev1.Secret{}). 737 + Owns(&networkingv1.Ingress{}). 738 + Complete(r) 739 + }
+971
internal/controller/knot_controller_test.go
··· 1 + package controller 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "testing" 7 + 8 + "github.com/stretchr/testify/assert" 9 + "github.com/stretchr/testify/require" 10 + appsv1 "k8s.io/api/apps/v1" 11 + corev1 "k8s.io/api/core/v1" 12 + networkingv1 "k8s.io/api/networking/v1" 13 + "k8s.io/apimachinery/pkg/api/resource" 14 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 + "k8s.io/apimachinery/pkg/runtime" 16 + "k8s.io/apimachinery/pkg/types" 17 + ctrl "sigs.k8s.io/controller-runtime" 18 + "sigs.k8s.io/controller-runtime/pkg/client" 19 + "sigs.k8s.io/controller-runtime/pkg/client/fake" 20 + 21 + tangledv1alpha1 "github.com/josie/knot-operator/api/v1alpha1" 22 + ) 23 + 24 + func newTestScheme() *runtime.Scheme { 25 + scheme := runtime.NewScheme() 26 + _ = tangledv1alpha1.AddToScheme(scheme) 27 + _ = corev1.AddToScheme(scheme) 28 + _ = appsv1.AddToScheme(scheme) 29 + _ = networkingv1.AddToScheme(scheme) 30 + return scheme 31 + } 32 + 33 + func newTestReconciler(objs ...client.Object) *KnotReconciler { 34 + scheme := newTestScheme() 35 + fakeClient := fake.NewClientBuilder(). 36 + WithScheme(scheme). 37 + WithObjects(objs...). 38 + WithStatusSubresource(&tangledv1alpha1.Knot{}). 39 + Build() 40 + return &KnotReconciler{ 41 + Client: fakeClient, 42 + Scheme: scheme, 43 + } 44 + } 45 + 46 + func newMinimalKnot(name, namespace string) *tangledv1alpha1.Knot { 47 + return &tangledv1alpha1.Knot{ 48 + ObjectMeta: metav1.ObjectMeta{ 49 + Name: name, 50 + Namespace: namespace, 51 + }, 52 + Spec: tangledv1alpha1.KnotSpec{ 53 + Hostname: "knot.example.com", 54 + Owner: "did:plc:test", 55 + }, 56 + } 57 + } 58 + 59 + func TestSetDefaults(t *testing.T) { 60 + tests := []struct { 61 + name string 62 + spec tangledv1alpha1.KnotSpec 63 + expected tangledv1alpha1.KnotSpec 64 + }{ 65 + { 66 + name: "all defaults applied", 67 + spec: tangledv1alpha1.KnotSpec{ 68 + Hostname: "knot.example.com", 69 + Owner: "did:plc:test", 70 + }, 71 + expected: tangledv1alpha1.KnotSpec{ 72 + Image: "docker.io/tngl/knot:v1.10.0-alpha", 73 + ImagePullPolicy: corev1.PullIfNotPresent, 74 + Replicas: 1, 75 + Hostname: "knot.example.com", 76 + Owner: "did:plc:test", 77 + AppviewEndpoint: "https://tangled.org", 78 + Storage: tangledv1alpha1.KnotStorageSpec{ 79 + RepoSize: "10Gi", 80 + DBSize: "1Gi", 81 + RepoPath: "/data/repos", 82 + DBPath: "/data/db", 83 + }, 84 + }, 85 + }, 86 + { 87 + name: "custom values preserved", 88 + spec: tangledv1alpha1.KnotSpec{ 89 + Image: "custom/image:v1.0.0", 90 + ImagePullPolicy: corev1.PullAlways, 91 + Replicas: 3, 92 + Hostname: "knot.example.com", 93 + Owner: "did:plc:test", 94 + AppviewEndpoint: "https://custom.endpoint.com", 95 + Storage: tangledv1alpha1.KnotStorageSpec{ 96 + RepoSize: "50Gi", 97 + DBSize: "5Gi", 98 + RepoPath: "/custom/repos", 99 + DBPath: "/custom/db", 100 + }, 101 + }, 102 + expected: tangledv1alpha1.KnotSpec{ 103 + Image: "custom/image:v1.0.0", 104 + ImagePullPolicy: corev1.PullAlways, 105 + Replicas: 3, 106 + Hostname: "knot.example.com", 107 + Owner: "did:plc:test", 108 + AppviewEndpoint: "https://custom.endpoint.com", 109 + Storage: tangledv1alpha1.KnotStorageSpec{ 110 + RepoSize: "50Gi", 111 + DBSize: "5Gi", 112 + RepoPath: "/custom/repos", 113 + DBPath: "/custom/db", 114 + }, 115 + }, 116 + }, 117 + { 118 + name: "partial defaults", 119 + spec: tangledv1alpha1.KnotSpec{ 120 + Image: "custom/image:v2.0.0", 121 + Replicas: 2, 122 + Hostname: "knot.example.com", 123 + Owner: "did:plc:test", 124 + Storage: tangledv1alpha1.KnotStorageSpec{ 125 + RepoSize: "20Gi", 126 + }, 127 + }, 128 + expected: tangledv1alpha1.KnotSpec{ 129 + Image: "custom/image:v2.0.0", 130 + ImagePullPolicy: corev1.PullIfNotPresent, 131 + Replicas: 2, 132 + Hostname: "knot.example.com", 133 + Owner: "did:plc:test", 134 + AppviewEndpoint: "https://tangled.org", 135 + Storage: tangledv1alpha1.KnotStorageSpec{ 136 + RepoSize: "20Gi", 137 + DBSize: "1Gi", 138 + RepoPath: "/data/repos", 139 + DBPath: "/data/db", 140 + }, 141 + }, 142 + }, 143 + } 144 + 145 + for _, tt := range tests { 146 + t.Run(tt.name, func(t *testing.T) { 147 + knot := &tangledv1alpha1.Knot{ 148 + Spec: tt.spec, 149 + } 150 + r := &KnotReconciler{} 151 + r.setDefaults(knot) 152 + 153 + assert.Equal(t, tt.expected.Image, knot.Spec.Image) 154 + assert.Equal(t, tt.expected.ImagePullPolicy, knot.Spec.ImagePullPolicy) 155 + assert.Equal(t, tt.expected.Replicas, knot.Spec.Replicas) 156 + assert.Equal(t, tt.expected.AppviewEndpoint, knot.Spec.AppviewEndpoint) 157 + assert.Equal(t, tt.expected.Storage.RepoSize, knot.Spec.Storage.RepoSize) 158 + assert.Equal(t, tt.expected.Storage.DBSize, knot.Spec.Storage.DBSize) 159 + assert.Equal(t, tt.expected.Storage.RepoPath, knot.Spec.Storage.RepoPath) 160 + assert.Equal(t, tt.expected.Storage.DBPath, knot.Spec.Storage.DBPath) 161 + }) 162 + } 163 + } 164 + 165 + func TestBuildPVC(t *testing.T) { 166 + tests := []struct { 167 + name string 168 + knotName string 169 + namespace string 170 + suffix string 171 + size string 172 + storageClass string 173 + expectedName string 174 + expectedStorageClass *string 175 + }{ 176 + { 177 + name: "repo PVC without storage class", 178 + knotName: "test-knot", 179 + namespace: "default", 180 + suffix: "repos", 181 + size: "10Gi", 182 + storageClass: "", 183 + expectedName: "test-knot-repos", 184 + expectedStorageClass: nil, 185 + }, 186 + { 187 + name: "repo PVC with custom storage class", 188 + knotName: "my-knot", 189 + namespace: "production", 190 + suffix: "repos", 191 + size: "100Gi", 192 + storageClass: "fast-storage", 193 + expectedName: "my-knot-repos", 194 + expectedStorageClass: func() *string { 195 + s := "fast-storage" 196 + return &s 197 + }(), 198 + }, 199 + { 200 + name: "db PVC without storage class", 201 + knotName: "test-knot", 202 + namespace: "default", 203 + suffix: "db", 204 + size: "1Gi", 205 + storageClass: "", 206 + expectedName: "test-knot-db", 207 + expectedStorageClass: nil, 208 + }, 209 + { 210 + name: "db PVC with custom storage class", 211 + knotName: "my-knot", 212 + namespace: "production", 213 + suffix: "db", 214 + size: "10Gi", 215 + storageClass: "ssd-storage", 216 + expectedName: "my-knot-db", 217 + expectedStorageClass: func() *string { 218 + s := "ssd-storage" 219 + return &s 220 + }(), 221 + }, 222 + } 223 + 224 + for _, tt := range tests { 225 + t.Run(tt.name, func(t *testing.T) { 226 + knot := newMinimalKnot(tt.knotName, tt.namespace) 227 + 228 + r := &KnotReconciler{} 229 + pvc := r.buildPVC(knot, tt.suffix, tt.size, tt.storageClass) 230 + 231 + assert.Equal(t, tt.expectedName, pvc.Name) 232 + assert.Equal(t, tt.namespace, pvc.Namespace) 233 + assert.Equal(t, []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, pvc.Spec.AccessModes) 234 + assert.Equal(t, resource.MustParse(tt.size), pvc.Spec.Resources.Requests[corev1.ResourceStorage]) 235 + 236 + if tt.expectedStorageClass == nil { 237 + assert.Nil(t, pvc.Spec.StorageClassName) 238 + } else { 239 + require.NotNil(t, pvc.Spec.StorageClassName) 240 + assert.Equal(t, *tt.expectedStorageClass, *pvc.Spec.StorageClassName) 241 + } 242 + 243 + assert.Equal(t, "knot", pvc.Labels["app.kubernetes.io/name"]) 244 + assert.Equal(t, tt.knotName, pvc.Labels["app.kubernetes.io/instance"]) 245 + }) 246 + } 247 + } 248 + 249 + func TestBuildDeployment(t *testing.T) { 250 + t.Run("basic deployment", func(t *testing.T) { 251 + knot := newMinimalKnot("test-knot", "default") 252 + knot.Spec.Image = "test/image:v1" 253 + knot.Spec.ImagePullPolicy = corev1.PullAlways 254 + knot.Spec.Replicas = 2 255 + knot.Spec.Storage.RepoPath = "/data/repos" 256 + knot.Spec.Storage.DBPath = "/data/db" 257 + 258 + r := &KnotReconciler{} 259 + dep := r.buildDeployment(knot) 260 + 261 + assert.Equal(t, "test-knot", dep.Name) 262 + assert.Equal(t, "default", dep.Namespace) 263 + assert.Equal(t, int32(2), *dep.Spec.Replicas) 264 + 265 + require.Len(t, dep.Spec.Template.Spec.Containers, 1) 266 + container := dep.Spec.Template.Spec.Containers[0] 267 + 268 + assert.Equal(t, "knot", container.Name) 269 + assert.Equal(t, "test/image:v1", container.Image) 270 + assert.Equal(t, corev1.PullAlways, container.ImagePullPolicy) 271 + 272 + assert.Len(t, container.Ports, 2) 273 + assert.Equal(t, int32(5555), container.Ports[0].ContainerPort) 274 + assert.Equal(t, int32(5444), container.Ports[1].ContainerPort) 275 + 276 + require.NotNil(t, container.LivenessProbe) 277 + assert.Equal(t, "/", container.LivenessProbe.HTTPGet.Path) 278 + assert.Equal(t, int32(5555), container.LivenessProbe.HTTPGet.Port.IntVal) 279 + 280 + require.NotNil(t, container.ReadinessProbe) 281 + assert.Equal(t, "/", container.ReadinessProbe.HTTPGet.Path) 282 + assert.Equal(t, int32(5555), container.ReadinessProbe.HTTPGet.Port.IntVal) 283 + 284 + assert.Len(t, dep.Spec.Template.Spec.Volumes, 3) 285 + assert.Len(t, container.VolumeMounts, 3) 286 + }) 287 + 288 + t.Run("deployment with SSH enabled", func(t *testing.T) { 289 + knot := newMinimalKnot("test-knot", "default") 290 + knot.Spec.Image = "test/image:v1" 291 + knot.Spec.Replicas = 1 292 + knot.Spec.Storage.RepoPath = "/data/repos" 293 + knot.Spec.Storage.DBPath = "/data/db" 294 + knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{ 295 + Enabled: true, 296 + } 297 + 298 + r := &KnotReconciler{} 299 + dep := r.buildDeployment(knot) 300 + 301 + container := dep.Spec.Template.Spec.Containers[0] 302 + assert.Len(t, container.Ports, 3) 303 + 304 + var sshPort *corev1.ContainerPort 305 + for i := range container.Ports { 306 + if container.Ports[i].Name == "ssh" { 307 + sshPort = &container.Ports[i] 308 + break 309 + } 310 + } 311 + require.NotNil(t, sshPort) 312 + assert.Equal(t, int32(22), sshPort.ContainerPort) 313 + }) 314 + 315 + t.Run("deployment with service account", func(t *testing.T) { 316 + knot := newMinimalKnot("test-knot", "default") 317 + knot.Spec.Image = "test/image:v1" 318 + knot.Spec.Replicas = 1 319 + knot.Spec.Storage.RepoPath = "/data/repos" 320 + knot.Spec.Storage.DBPath = "/data/db" 321 + knot.Spec.ServiceAccountName = "custom-sa" 322 + 323 + r := &KnotReconciler{} 324 + dep := r.buildDeployment(knot) 325 + 326 + assert.Equal(t, "custom-sa", dep.Spec.Template.Spec.ServiceAccountName) 327 + }) 328 + } 329 + 330 + func TestReconcileConfigMap(t *testing.T) { 331 + ctx := context.Background() 332 + 333 + t.Run("creates configmap when not exists", func(t *testing.T) { 334 + knot := newMinimalKnot("test-knot", "default") 335 + knot.Spec.Hostname = "knot.example.com" 336 + knot.Spec.Owner = "did:plc:test" 337 + knot.Spec.AppviewEndpoint = "https://tangled.org" 338 + knot.Spec.Storage.RepoPath = "/data/repos" 339 + knot.Spec.Storage.DBPath = "/data/db" 340 + 341 + r := newTestReconciler(knot) 342 + err := r.reconcileConfigMap(ctx, knot) 343 + require.NoError(t, err) 344 + 345 + cm := &corev1.ConfigMap{} 346 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-config", Namespace: "default"}, cm) 347 + require.NoError(t, err) 348 + 349 + assert.Equal(t, "knot.example.com", cm.Data["KNOT_SERVER_HOSTNAME"]) 350 + assert.Equal(t, "did:plc:test", cm.Data["KNOT_SERVER_OWNER"]) 351 + assert.Equal(t, "https://tangled.org", cm.Data["APPVIEW_ENDPOINT"]) 352 + assert.Equal(t, "/data/repos", cm.Data["KNOT_REPO_SCAN_PATH"]) 353 + assert.Equal(t, "/data/db/knotserver.db", cm.Data["KNOT_SERVER_DB_PATH"]) 354 + assert.Equal(t, "0.0.0.0:5444", cm.Data["KNOT_SERVER_INTERNAL_LISTEN_ADDR"]) 355 + assert.Equal(t, "0.0.0.0:5555", cm.Data["KNOT_SERVER_LISTEN_ADDR"]) 356 + }) 357 + 358 + t.Run("updates configmap when exists", func(t *testing.T) { 359 + existingCM := &corev1.ConfigMap{ 360 + ObjectMeta: metav1.ObjectMeta{ 361 + Name: "test-knot-config", 362 + Namespace: "default", 363 + }, 364 + Data: map[string]string{ 365 + "KNOT_SERVER_HOSTNAME": "old.example.com", 366 + }, 367 + } 368 + 369 + knot := newMinimalKnot("test-knot", "default") 370 + knot.Spec.Hostname = "new.example.com" 371 + knot.Spec.Owner = "did:plc:new" 372 + knot.Spec.AppviewEndpoint = "https://new.tangled.org" 373 + knot.Spec.Storage.RepoPath = "/new/repos" 374 + knot.Spec.Storage.DBPath = "/new/db" 375 + 376 + r := newTestReconciler(knot, existingCM) 377 + err := r.reconcileConfigMap(ctx, knot) 378 + require.NoError(t, err) 379 + 380 + cm := &corev1.ConfigMap{} 381 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-config", Namespace: "default"}, cm) 382 + require.NoError(t, err) 383 + 384 + assert.Equal(t, "new.example.com", cm.Data["KNOT_SERVER_HOSTNAME"]) 385 + assert.Equal(t, "did:plc:new", cm.Data["KNOT_SERVER_OWNER"]) 386 + }) 387 + } 388 + 389 + func TestReconcileSecret(t *testing.T) { 390 + ctx := context.Background() 391 + 392 + t.Run("creates secret when not exists", func(t *testing.T) { 393 + knot := newMinimalKnot("test-knot", "default") 394 + 395 + r := newTestReconciler(knot) 396 + err := r.reconcileSecret(ctx, knot) 397 + require.NoError(t, err) 398 + 399 + secret := &corev1.Secret{} 400 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 401 + require.NoError(t, err) 402 + 403 + var serverSecret string 404 + if val, ok := secret.Data["KNOT_SERVER_SECRET"]; ok { 405 + serverSecret = string(val) 406 + } else if val, ok := secret.StringData["KNOT_SERVER_SECRET"]; ok { 407 + serverSecret = val 408 + } 409 + assert.Len(t, serverSecret, 64) 410 + }) 411 + 412 + t.Run("skips existing secret", func(t *testing.T) { 413 + existingSecret := &corev1.Secret{ 414 + ObjectMeta: metav1.ObjectMeta{ 415 + Name: "test-knot-secret", 416 + Namespace: "default", 417 + }, 418 + Data: map[string][]byte{ 419 + "KNOT_SERVER_SECRET": []byte("existing-secret-value"), 420 + }, 421 + } 422 + 423 + knot := newMinimalKnot("test-knot", "default") 424 + 425 + r := newTestReconciler(knot, existingSecret) 426 + err := r.reconcileSecret(ctx, knot) 427 + require.NoError(t, err) 428 + 429 + secret := &corev1.Secret{} 430 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 431 + require.NoError(t, err) 432 + 433 + assert.Equal(t, "existing-secret-value", string(secret.Data["KNOT_SERVER_SECRET"])) 434 + }) 435 + 436 + t.Run("regenerates secret with empty value", func(t *testing.T) { 437 + existingSecret := &corev1.Secret{ 438 + ObjectMeta: metav1.ObjectMeta{ 439 + Name: "test-knot-secret", 440 + Namespace: "default", 441 + }, 442 + Data: map[string][]byte{ 443 + "KNOT_SERVER_SECRET": []byte(""), 444 + }, 445 + } 446 + 447 + knot := newMinimalKnot("test-knot", "default") 448 + 449 + r := newTestReconciler(knot, existingSecret) 450 + err := r.reconcileSecret(ctx, knot) 451 + require.NoError(t, err) 452 + 453 + secret := &corev1.Secret{} 454 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 455 + require.NoError(t, err) 456 + 457 + var serverSecret string 458 + if val, ok := secret.Data["KNOT_SERVER_SECRET"]; ok { 459 + serverSecret = string(val) 460 + } else if val, ok := secret.StringData["KNOT_SERVER_SECRET"]; ok { 461 + serverSecret = val 462 + } 463 + assert.Len(t, serverSecret, 64) 464 + }) 465 + } 466 + 467 + func TestReconcileService(t *testing.T) { 468 + ctx := context.Background() 469 + 470 + t.Run("creates service when not exists", func(t *testing.T) { 471 + knot := newMinimalKnot("test-knot", "default") 472 + 473 + r := newTestReconciler(knot) 474 + err := r.reconcileService(ctx, knot) 475 + require.NoError(t, err) 476 + 477 + svc := &corev1.Service{} 478 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, svc) 479 + require.NoError(t, err) 480 + 481 + assert.Equal(t, corev1.ServiceTypeClusterIP, svc.Spec.Type) 482 + require.Len(t, svc.Spec.Ports, 2) 483 + 484 + var httpPort, internalPort *corev1.ServicePort 485 + for i := range svc.Spec.Ports { 486 + switch svc.Spec.Ports[i].Name { 487 + case "http": 488 + httpPort = &svc.Spec.Ports[i] 489 + case "internal": 490 + internalPort = &svc.Spec.Ports[i] 491 + } 492 + } 493 + 494 + require.NotNil(t, httpPort) 495 + assert.Equal(t, int32(5555), httpPort.Port) 496 + 497 + require.NotNil(t, internalPort) 498 + assert.Equal(t, int32(5444), internalPort.Port) 499 + }) 500 + 501 + t.Run("updates existing service", func(t *testing.T) { 502 + existingSvc := &corev1.Service{ 503 + ObjectMeta: metav1.ObjectMeta{ 504 + Name: "test-knot", 505 + Namespace: "default", 506 + }, 507 + Spec: corev1.ServiceSpec{ 508 + Ports: []corev1.ServicePort{ 509 + {Name: "old-port", Port: 8080}, 510 + }, 511 + }, 512 + } 513 + 514 + knot := newMinimalKnot("test-knot", "default") 515 + 516 + r := newTestReconciler(knot, existingSvc) 517 + err := r.reconcileService(ctx, knot) 518 + require.NoError(t, err) 519 + 520 + svc := &corev1.Service{} 521 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, svc) 522 + require.NoError(t, err) 523 + 524 + assert.Len(t, svc.Spec.Ports, 2) 525 + }) 526 + } 527 + 528 + func TestReconcileSSHService(t *testing.T) { 529 + ctx := context.Background() 530 + 531 + t.Run("returns nil when SSH spec is nil", func(t *testing.T) { 532 + knot := newMinimalKnot("test-knot", "default") 533 + knot.Spec.SSH = nil 534 + 535 + r := newTestReconciler(knot) 536 + err := r.reconcileSSHService(ctx, knot) 537 + require.NoError(t, err) 538 + 539 + svcList := &corev1.ServiceList{} 540 + err = r.List(ctx, svcList) 541 + require.NoError(t, err) 542 + assert.Empty(t, svcList.Items) 543 + }) 544 + 545 + t.Run("creates SSH service with defaults", func(t *testing.T) { 546 + knot := newMinimalKnot("test-knot", "default") 547 + knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{ 548 + Enabled: true, 549 + } 550 + 551 + r := newTestReconciler(knot) 552 + err := r.reconcileSSHService(ctx, knot) 553 + require.NoError(t, err) 554 + 555 + svc := &corev1.Service{} 556 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, svc) 557 + require.NoError(t, err) 558 + 559 + assert.Equal(t, corev1.ServiceTypeLoadBalancer, svc.Spec.Type) 560 + require.Len(t, svc.Spec.Ports, 1) 561 + assert.Equal(t, int32(22), svc.Spec.Ports[0].Port) 562 + assert.Equal(t, "ssh", svc.Spec.Ports[0].Name) 563 + }) 564 + 565 + t.Run("creates SSH service with custom port and NodePort type", func(t *testing.T) { 566 + knot := newMinimalKnot("test-knot", "default") 567 + knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{ 568 + Enabled: true, 569 + Port: 2222, 570 + ServiceType: corev1.ServiceTypeNodePort, 571 + NodePort: 30022, 572 + } 573 + 574 + r := newTestReconciler(knot) 575 + err := r.reconcileSSHService(ctx, knot) 576 + require.NoError(t, err) 577 + 578 + svc := &corev1.Service{} 579 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, svc) 580 + require.NoError(t, err) 581 + 582 + assert.Equal(t, corev1.ServiceTypeNodePort, svc.Spec.Type) 583 + assert.Equal(t, int32(2222), svc.Spec.Ports[0].Port) 584 + assert.Equal(t, int32(30022), svc.Spec.Ports[0].NodePort) 585 + }) 586 + 587 + t.Run("creates SSH service with LoadBalancer IP", func(t *testing.T) { 588 + knot := newMinimalKnot("test-knot", "default") 589 + knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{ 590 + Enabled: true, 591 + ServiceType: corev1.ServiceTypeLoadBalancer, 592 + LoadBalancerIP: "10.0.0.100", 593 + } 594 + 595 + r := newTestReconciler(knot) 596 + err := r.reconcileSSHService(ctx, knot) 597 + require.NoError(t, err) 598 + 599 + svc := &corev1.Service{} 600 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, svc) 601 + require.NoError(t, err) 602 + 603 + assert.Equal(t, "10.0.0.100", svc.Spec.LoadBalancerIP) 604 + }) 605 + } 606 + 607 + func TestReconcileIngress(t *testing.T) { 608 + ctx := context.Background() 609 + 610 + t.Run("creates ingress", func(t *testing.T) { 611 + knot := newMinimalKnot("test-knot", "default") 612 + knot.Spec.Hostname = "knot.example.com" 613 + knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{ 614 + Enabled: true, 615 + IngressClassName: "nginx", 616 + Annotations: map[string]string{ 617 + "nginx.ingress.kubernetes.io/ssl-redirect": "true", 618 + }, 619 + } 620 + 621 + r := newTestReconciler(knot) 622 + err := r.reconcileIngress(ctx, knot) 623 + require.NoError(t, err) 624 + 625 + ingress := &networkingv1.Ingress{} 626 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress) 627 + require.NoError(t, err) 628 + 629 + require.NotNil(t, ingress.Spec.IngressClassName) 630 + assert.Equal(t, "nginx", *ingress.Spec.IngressClassName) 631 + assert.Equal(t, "true", ingress.Annotations["nginx.ingress.kubernetes.io/ssl-redirect"]) 632 + 633 + require.Len(t, ingress.Spec.Rules, 1) 634 + assert.Equal(t, "knot.example.com", ingress.Spec.Rules[0].Host) 635 + require.NotNil(t, ingress.Spec.Rules[0].HTTP) 636 + require.Len(t, ingress.Spec.Rules[0].HTTP.Paths, 1) 637 + assert.Equal(t, "/", ingress.Spec.Rules[0].HTTP.Paths[0].Path) 638 + assert.Equal(t, int32(5555), ingress.Spec.Rules[0].HTTP.Paths[0].Backend.Service.Port.Number) 639 + }) 640 + 641 + t.Run("creates ingress with TLS", func(t *testing.T) { 642 + knot := newMinimalKnot("test-knot", "default") 643 + knot.Spec.Hostname = "knot.example.com" 644 + knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{ 645 + Enabled: true, 646 + TLS: &tangledv1alpha1.KnotIngressTLSSpec{ 647 + Enabled: true, 648 + SecretName: "knot-tls", 649 + }, 650 + } 651 + 652 + r := newTestReconciler(knot) 653 + err := r.reconcileIngress(ctx, knot) 654 + require.NoError(t, err) 655 + 656 + ingress := &networkingv1.Ingress{} 657 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress) 658 + require.NoError(t, err) 659 + 660 + require.Len(t, ingress.Spec.TLS, 1) 661 + assert.Equal(t, []string{"knot.example.com"}, ingress.Spec.TLS[0].Hosts) 662 + assert.Equal(t, "knot-tls", ingress.Spec.TLS[0].SecretName) 663 + }) 664 + 665 + t.Run("updates existing ingress", func(t *testing.T) { 666 + existingIngress := &networkingv1.Ingress{ 667 + ObjectMeta: metav1.ObjectMeta{ 668 + Name: "test-knot", 669 + Namespace: "default", 670 + }, 671 + } 672 + 673 + knot := newMinimalKnot("test-knot", "default") 674 + knot.Spec.Hostname = "new.example.com" 675 + knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{ 676 + Enabled: true, 677 + Annotations: map[string]string{ 678 + "new-annotation": "value", 679 + }, 680 + } 681 + 682 + r := newTestReconciler(knot, existingIngress) 683 + err := r.reconcileIngress(ctx, knot) 684 + require.NoError(t, err) 685 + 686 + ingress := &networkingv1.Ingress{} 687 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress) 688 + require.NoError(t, err) 689 + 690 + assert.Equal(t, "value", ingress.Annotations["new-annotation"]) 691 + assert.Equal(t, "new.example.com", ingress.Spec.Rules[0].Host) 692 + }) 693 + } 694 + 695 + func TestReconcile(t *testing.T) { 696 + ctx := context.Background() 697 + 698 + t.Run("returns empty result when knot not found", func(t *testing.T) { 699 + r := newTestReconciler() 700 + result, err := r.Reconcile(ctx, ctrl.Request{ 701 + NamespacedName: types.NamespacedName{Name: "nonexistent", Namespace: "default"}, 702 + }) 703 + 704 + require.NoError(t, err) 705 + assert.Equal(t, ctrl.Result{}, result) 706 + }) 707 + 708 + t.Run("adds finalizer on new knot", func(t *testing.T) { 709 + knot := newMinimalKnot("test-knot", "default") 710 + 711 + r := newTestReconciler(knot) 712 + 713 + r.setDefaults(knot) 714 + 715 + _, err := r.Reconcile(ctx, ctrl.Request{ 716 + NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"}, 717 + }) 718 + require.NoError(t, err) 719 + 720 + updatedKnot := &tangledv1alpha1.Knot{} 721 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, updatedKnot) 722 + require.NoError(t, err) 723 + 724 + assert.Contains(t, updatedKnot.Finalizers, knotFinalizer) 725 + }) 726 + 727 + t.Run("creates all required resources", func(t *testing.T) { 728 + knot := newMinimalKnot("test-knot", "default") 729 + knot.Finalizers = []string{knotFinalizer} 730 + 731 + r := newTestReconciler(knot) 732 + 733 + _, err := r.Reconcile(ctx, ctrl.Request{ 734 + NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"}, 735 + }) 736 + require.NoError(t, err) 737 + 738 + repoPVC := &corev1.PersistentVolumeClaim{} 739 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-repos", Namespace: "default"}, repoPVC) 740 + require.NoError(t, err) 741 + 742 + dbPVC := &corev1.PersistentVolumeClaim{} 743 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-db", Namespace: "default"}, dbPVC) 744 + require.NoError(t, err) 745 + 746 + cm := &corev1.ConfigMap{} 747 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-config", Namespace: "default"}, cm) 748 + require.NoError(t, err) 749 + 750 + secret := &corev1.Secret{} 751 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 752 + require.NoError(t, err) 753 + 754 + svc := &corev1.Service{} 755 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, svc) 756 + require.NoError(t, err) 757 + 758 + dep := &appsv1.Deployment{} 759 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, dep) 760 + require.NoError(t, err) 761 + }) 762 + 763 + t.Run("creates SSH service when enabled", func(t *testing.T) { 764 + knot := newMinimalKnot("test-knot", "default") 765 + knot.Finalizers = []string{knotFinalizer} 766 + knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{ 767 + Enabled: true, 768 + } 769 + 770 + r := newTestReconciler(knot) 771 + 772 + _, err := r.Reconcile(ctx, ctrl.Request{ 773 + NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"}, 774 + }) 775 + require.NoError(t, err) 776 + 777 + sshSvc := &corev1.Service{} 778 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-ssh", Namespace: "default"}, sshSvc) 779 + require.NoError(t, err) 780 + }) 781 + 782 + t.Run("creates ingress when enabled", func(t *testing.T) { 783 + knot := newMinimalKnot("test-knot", "default") 784 + knot.Finalizers = []string{knotFinalizer} 785 + knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{ 786 + Enabled: true, 787 + } 788 + 789 + r := newTestReconciler(knot) 790 + 791 + _, err := r.Reconcile(ctx, ctrl.Request{ 792 + NamespacedName: types.NamespacedName{Name: "test-knot", Namespace: "default"}, 793 + }) 794 + require.NoError(t, err) 795 + 796 + ingress := &networkingv1.Ingress{} 797 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot", Namespace: "default"}, ingress) 798 + require.NoError(t, err) 799 + }) 800 + } 801 + 802 + func TestUpdateStatus(t *testing.T) { 803 + ctx := context.Background() 804 + 805 + t.Run("sets URL with http when no TLS", func(t *testing.T) { 806 + knot := newMinimalKnot("test-knot", "default") 807 + knot.Spec.Hostname = "knot.example.com" 808 + knot.Spec.Replicas = 1 809 + knot.Status.Conditions = []metav1.Condition{} 810 + 811 + r := newTestReconciler(knot) 812 + 813 + _, err := r.updateStatus(ctx, knot) 814 + require.NoError(t, err) 815 + 816 + assert.Equal(t, "http://knot.example.com", knot.Status.URL) 817 + }) 818 + 819 + t.Run("sets URL with https when TLS enabled", func(t *testing.T) { 820 + knot := newMinimalKnot("test-knot", "default") 821 + knot.Spec.Hostname = "knot.example.com" 822 + knot.Spec.Replicas = 1 823 + knot.Spec.Ingress = &tangledv1alpha1.KnotIngressSpec{ 824 + Enabled: true, 825 + TLS: &tangledv1alpha1.KnotIngressTLSSpec{ 826 + Enabled: true, 827 + }, 828 + } 829 + knot.Status.Conditions = []metav1.Condition{} 830 + 831 + r := newTestReconciler(knot) 832 + 833 + _, err := r.updateStatus(ctx, knot) 834 + require.NoError(t, err) 835 + 836 + assert.Equal(t, "https://knot.example.com", knot.Status.URL) 837 + }) 838 + 839 + t.Run("sets SSH URL when SSH enabled", func(t *testing.T) { 840 + knot := newMinimalKnot("test-knot", "default") 841 + knot.Spec.Hostname = "knot.example.com" 842 + knot.Spec.Replicas = 1 843 + knot.Spec.SSH = &tangledv1alpha1.KnotSSHSpec{ 844 + Enabled: true, 845 + } 846 + knot.Status.Conditions = []metav1.Condition{} 847 + 848 + r := newTestReconciler(knot) 849 + 850 + _, err := r.updateStatus(ctx, knot) 851 + require.NoError(t, err) 852 + 853 + assert.Equal(t, "git@knot.example.com", knot.Status.SSHURL) 854 + }) 855 + 856 + t.Run("sets phase to Pending when not all replicas ready", func(t *testing.T) { 857 + knot := newMinimalKnot("test-knot", "default") 858 + knot.Spec.Hostname = "knot.example.com" 859 + knot.Spec.Replicas = 3 860 + knot.Status.ReadyReplicas = 1 861 + knot.Status.Conditions = []metav1.Condition{} 862 + 863 + r := newTestReconciler(knot) 864 + 865 + _, err := r.updateStatus(ctx, knot) 866 + require.NoError(t, err) 867 + 868 + assert.Equal(t, "Pending", knot.Status.Phase) 869 + }) 870 + 871 + t.Run("sets phase to Running when all replicas ready", func(t *testing.T) { 872 + knot := newMinimalKnot("test-knot", "default") 873 + knot.Spec.Hostname = "knot.example.com" 874 + knot.Spec.Replicas = 2 875 + 876 + dep := &appsv1.Deployment{ 877 + ObjectMeta: metav1.ObjectMeta{ 878 + Name: "test-knot", 879 + Namespace: "default", 880 + }, 881 + Status: appsv1.DeploymentStatus{ 882 + ReadyReplicas: 2, 883 + AvailableReplicas: 2, 884 + }, 885 + } 886 + 887 + knot.Status.Conditions = []metav1.Condition{} 888 + 889 + r := newTestReconciler(knot, dep) 890 + 891 + _, err := r.updateStatus(ctx, knot) 892 + require.NoError(t, err) 893 + 894 + assert.Equal(t, "Running", knot.Status.Phase) 895 + assert.Equal(t, int32(2), knot.Status.ReadyReplicas) 896 + }) 897 + } 898 + 899 + func TestHandleError(t *testing.T) { 900 + ctx := context.Background() 901 + 902 + t.Run("sets degraded condition and failed phase", func(t *testing.T) { 903 + knot := newMinimalKnot("test-knot", "default") 904 + knot.Status.Conditions = []metav1.Condition{} 905 + 906 + r := newTestReconciler(knot) 907 + 908 + testErr := fmt.Errorf("test error") 909 + result, err := r.handleError(ctx, knot, testErr, "Test failure message") 910 + 911 + assert.Error(t, err) 912 + assert.Equal(t, testErr, err) 913 + assert.Equal(t, defaultRequeueTime, result.RequeueAfter) 914 + 915 + assert.Equal(t, "Failed", knot.Status.Phase) 916 + 917 + var degradedCondition *metav1.Condition 918 + for i := range knot.Status.Conditions { 919 + if knot.Status.Conditions[i].Type == typeDegraded { 920 + degradedCondition = &knot.Status.Conditions[i] 921 + break 922 + } 923 + } 924 + require.NotNil(t, degradedCondition) 925 + assert.Equal(t, metav1.ConditionTrue, degradedCondition.Status) 926 + assert.Equal(t, "ReconciliationFailed", degradedCondition.Reason) 927 + assert.Contains(t, degradedCondition.Message, "Test failure message") 928 + assert.Contains(t, degradedCondition.Message, "test error") 929 + }) 930 + } 931 + 932 + func TestGenerateRandomHex(t *testing.T) { 933 + t.Run("generates correct length", func(t *testing.T) { 934 + hex, err := generateRandomHex(32) 935 + require.NoError(t, err) 936 + assert.Len(t, hex, 64) 937 + }) 938 + 939 + t.Run("generates unique values", func(t *testing.T) { 940 + hex1, err := generateRandomHex(32) 941 + require.NoError(t, err) 942 + 943 + hex2, err := generateRandomHex(32) 944 + require.NoError(t, err) 945 + 946 + assert.NotEqual(t, hex1, hex2) 947 + }) 948 + } 949 + 950 + func TestLabelsForKnot(t *testing.T) { 951 + knot := newMinimalKnot("my-knot", "default") 952 + r := &KnotReconciler{} 953 + 954 + labels := r.labelsForKnot(knot) 955 + 956 + assert.Equal(t, "knot", labels["app.kubernetes.io/name"]) 957 + assert.Equal(t, "my-knot", labels["app.kubernetes.io/instance"]) 958 + assert.Equal(t, "knot-operator", labels["app.kubernetes.io/managed-by"]) 959 + assert.Equal(t, "server", labels["app.kubernetes.io/component"]) 960 + } 961 + 962 + func TestSelectorLabelsForKnot(t *testing.T) { 963 + knot := newMinimalKnot("my-knot", "default") 964 + r := &KnotReconciler{} 965 + 966 + labels := r.selectorLabelsForKnot(knot) 967 + 968 + assert.Equal(t, "knot", labels["app.kubernetes.io/name"]) 969 + assert.Equal(t, "my-knot", labels["app.kubernetes.io/instance"]) 970 + assert.Len(t, labels, 2) 971 + }
+224
internal/controller/openshift.go
··· 1 + package controller 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + 7 + "k8s.io/apimachinery/pkg/api/errors" 8 + "k8s.io/apimachinery/pkg/api/meta" 9 + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 + "k8s.io/apimachinery/pkg/runtime/schema" 11 + "k8s.io/apimachinery/pkg/types" 12 + "sigs.k8s.io/controller-runtime/pkg/client" 13 + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 14 + "sigs.k8s.io/controller-runtime/pkg/log" 15 + 16 + tangledv1alpha1 "github.com/josie/knot-operator/api/v1alpha1" 17 + ) 18 + 19 + var ( 20 + routeGVK = schema.GroupVersionKind{ 21 + Group: "route.openshift.io", 22 + Version: "v1", 23 + Kind: "Route", 24 + } 25 + sccGVK = schema.GroupVersionKind{ 26 + Group: "security.openshift.io", 27 + Version: "v1", 28 + Kind: "SecurityContextConstraints", 29 + } 30 + ) 31 + 32 + func stringDefault(val, defaultVal string) string { 33 + if val == "" { 34 + return defaultVal 35 + } 36 + return val 37 + } 38 + 39 + func (r *KnotReconciler) reconcileRoute(ctx context.Context, knot *tangledv1alpha1.Knot) error { 40 + logger := r.loggerFor(ctx, knot) 41 + routeSpec := knot.Spec.OpenShift.Route 42 + 43 + route := &unstructured.Unstructured{} 44 + route.SetGroupVersionKind(routeGVK) 45 + route.SetName(knot.Name) 46 + route.SetNamespace(knot.Namespace) 47 + route.SetLabels(r.labelsForKnot(knot)) 48 + 49 + if routeSpec.Annotations != nil { 50 + route.SetAnnotations(routeSpec.Annotations) 51 + } 52 + 53 + spec := map[string]interface{}{ 54 + "host": knot.Spec.Hostname, 55 + "to": map[string]interface{}{ 56 + "kind": "Service", 57 + "name": knot.Name, 58 + "weight": int64(100), 59 + }, 60 + "port": map[string]interface{}{ 61 + "targetPort": "http", 62 + }, 63 + "wildcardPolicy": routeSpec.WildcardPolicy, 64 + } 65 + 66 + if routeSpec.TLS != nil { 67 + tlsConfig := map[string]interface{}{ 68 + "termination": routeSpec.TLS.Termination, 69 + } 70 + 71 + if routeSpec.TLS.InsecureEdgeTerminationPolicy != "" { 72 + tlsConfig["insecureEdgeTerminationPolicy"] = routeSpec.TLS.InsecureEdgeTerminationPolicy 73 + } 74 + if routeSpec.TLS.Certificate != "" { 75 + tlsConfig["certificate"] = routeSpec.TLS.Certificate 76 + } 77 + if routeSpec.TLS.Key != "" { 78 + tlsConfig["key"] = routeSpec.TLS.Key 79 + } 80 + if routeSpec.TLS.CACertificate != "" { 81 + tlsConfig["caCertificate"] = routeSpec.TLS.CACertificate 82 + } 83 + if routeSpec.TLS.DestinationCACertificate != "" { 84 + tlsConfig["destinationCACertificate"] = routeSpec.TLS.DestinationCACertificate 85 + } 86 + 87 + spec["tls"] = tlsConfig 88 + } 89 + 90 + if err := unstructured.SetNestedMap(route.Object, spec, "spec"); err != nil { 91 + return err 92 + } 93 + 94 + if err := controllerutil.SetControllerReference(knot, route, r.Scheme); err != nil { 95 + return err 96 + } 97 + 98 + existing := &unstructured.Unstructured{} 99 + existing.SetGroupVersionKind(routeGVK) 100 + err := r.Get(ctx, types.NamespacedName{Name: route.GetName(), Namespace: route.GetNamespace()}, existing) 101 + if err != nil { 102 + if errors.IsNotFound(err) { 103 + logger.V(1).Info("creating route", "route", route.GetName()) 104 + return r.Create(ctx, route) 105 + } 106 + return err 107 + } 108 + 109 + existing.Object["spec"] = route.Object["spec"] 110 + existing.SetAnnotations(route.GetAnnotations()) 111 + logger.V(1).Info("updating route", "route", route.GetName()) 112 + return r.Update(ctx, existing) 113 + } 114 + 115 + func (r *KnotReconciler) reconcileSCC(ctx context.Context, knot *tangledv1alpha1.Knot) error { 116 + logger := r.loggerFor(ctx, knot) 117 + sccSpec := knot.Spec.OpenShift.SCC 118 + 119 + sccName := stringDefault(sccSpec.Name, fmt.Sprintf("%s-scc", knot.Name)) 120 + 121 + scc := &unstructured.Unstructured{} 122 + scc.SetGroupVersionKind(sccGVK) 123 + scc.SetName(sccName) 124 + scc.SetLabels(r.labelsForKnot(knot)) 125 + 126 + volumes := sccSpec.Volumes 127 + if len(volumes) == 0 { 128 + volumes = []string{"configMap", "downwardAPI", "emptyDir", "persistentVolumeClaim", "projected", "secret"} 129 + } 130 + 131 + volumeInterfaces := make([]interface{}, len(volumes)) 132 + for i, v := range volumes { 133 + volumeInterfaces[i] = v 134 + } 135 + 136 + serviceAccountName := stringDefault(knot.Spec.ServiceAccountName, "default") 137 + 138 + sccData := map[string]interface{}{ 139 + "allowPrivilegedContainer": sccSpec.AllowPrivilegedContainer, 140 + "allowHostNetwork": sccSpec.AllowHostNetwork, 141 + "allowHostPorts": sccSpec.AllowHostPorts, 142 + "allowHostPID": sccSpec.AllowHostPID, 143 + "allowHostIPC": sccSpec.AllowHostIPC, 144 + "readOnlyRootFilesystem": sccSpec.ReadOnlyRootFilesystem, 145 + "runAsUser": map[string]interface{}{ 146 + "type": stringDefault(sccSpec.RunAsUser, "MustRunAsNonRoot"), 147 + }, 148 + "seLinuxContext": map[string]interface{}{ 149 + "type": stringDefault(sccSpec.SELinuxContext, "MustRunAs"), 150 + }, 151 + "fsGroup": map[string]interface{}{ 152 + "type": stringDefault(sccSpec.FSGroup, "MustRunAs"), 153 + }, 154 + "supplementalGroups": map[string]interface{}{ 155 + "type": stringDefault(sccSpec.SupplementalGroups, "RunAsAny"), 156 + }, 157 + "volumes": volumeInterfaces, 158 + "users": []interface{}{ 159 + fmt.Sprintf("system:serviceaccount:%s:%s", knot.Namespace, serviceAccountName), 160 + }, 161 + } 162 + 163 + for k, v := range sccData { 164 + if err := unstructured.SetNestedField(scc.Object, v, k); err != nil { 165 + return err 166 + } 167 + } 168 + 169 + scc.Object["metadata"] = map[string]interface{}{ 170 + "name": sccName, 171 + "labels": r.labelsForKnot(knot), 172 + "annotations": map[string]interface{}{ 173 + "tangled.org/managed-by": fmt.Sprintf("%s/%s", knot.Namespace, knot.Name), 174 + }, 175 + } 176 + 177 + existing := &unstructured.Unstructured{} 178 + existing.SetGroupVersionKind(sccGVK) 179 + err := r.Get(ctx, types.NamespacedName{Name: sccName}, existing) 180 + if err != nil { 181 + if errors.IsNotFound(err) { 182 + logger.V(1).Info("creating scc", "scc", sccName) 183 + return r.Create(ctx, scc) 184 + } 185 + return err 186 + } 187 + 188 + for k, v := range sccData { 189 + if err := unstructured.SetNestedField(existing.Object, v, k); err != nil { 190 + return err 191 + } 192 + } 193 + existing.SetLabels(r.labelsForKnot(knot)) 194 + existing.SetAnnotations(map[string]string{ 195 + "tangled.org/managed-by": fmt.Sprintf("%s/%s", knot.Namespace, knot.Name), 196 + }) 197 + existing.SetOwnerReferences(nil) 198 + logger.V(1).Info("updating scc", "scc", sccName) 199 + return r.Update(ctx, existing) 200 + } 201 + 202 + func (r *KnotReconciler) DetectOpenShift(ctx context.Context) bool { 203 + logger := log.FromContext(ctx).WithName("openshift-detection") 204 + 205 + routeList := &unstructured.UnstructuredList{} 206 + routeList.SetGroupVersionKind(schema.GroupVersionKind{ 207 + Group: "route.openshift.io", 208 + Version: "v1", 209 + Kind: "RouteList", 210 + }) 211 + 212 + err := r.List(ctx, routeList, &client.ListOptions{Limit: 1}) 213 + if err != nil { 214 + if meta.IsNoMatchError(err) { 215 + logger.Info("route API not available, running in kubernetes mode") 216 + return false 217 + } 218 + logger.Info("cluster detection failed, assuming kubernetes", "error", err.Error()) 219 + return false 220 + } 221 + 222 + logger.Info("route API available, running in openshift mode") 223 + return true 224 + }
+529
internal/controller/openshift_test.go
··· 1 + package controller 2 + 3 + import ( 4 + "context" 5 + "testing" 6 + 7 + "github.com/stretchr/testify/assert" 8 + "github.com/stretchr/testify/require" 9 + corev1 "k8s.io/api/core/v1" 10 + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 + "k8s.io/apimachinery/pkg/types" 12 + ctrl "sigs.k8s.io/controller-runtime" 13 + 14 + tangledv1alpha1 "github.com/josie/knot-operator/api/v1alpha1" 15 + ) 16 + 17 + func TestRouteGVK(t *testing.T) { 18 + t.Run("has correct values", func(t *testing.T) { 19 + assert.Equal(t, "route.openshift.io", routeGVK.Group) 20 + assert.Equal(t, "v1", routeGVK.Version) 21 + assert.Equal(t, "Route", routeGVK.Kind) 22 + }) 23 + } 24 + 25 + func TestSCCGVK(t *testing.T) { 26 + t.Run("has correct values", func(t *testing.T) { 27 + assert.Equal(t, "security.openshift.io", sccGVK.Group) 28 + assert.Equal(t, "v1", sccGVK.Version) 29 + assert.Equal(t, "SecurityContextConstraints", sccGVK.Kind) 30 + }) 31 + } 32 + 33 + func TestBuildRouteSpec(t *testing.T) { 34 + t.Run("builds basic route spec correctly", func(t *testing.T) { 35 + knot := newMinimalKnot("test-knot", "default") 36 + knot.Spec.Hostname = "knot.example.com" 37 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 38 + Route: &tangledv1alpha1.KnotRouteSpec{ 39 + Enabled: true, 40 + WildcardPolicy: "None", 41 + }, 42 + } 43 + 44 + routeSpec := knot.Spec.OpenShift.Route 45 + 46 + assert.True(t, routeSpec.Enabled) 47 + assert.Equal(t, "None", routeSpec.WildcardPolicy) 48 + assert.Nil(t, routeSpec.TLS) 49 + }) 50 + 51 + t.Run("builds route spec with TLS correctly", func(t *testing.T) { 52 + knot := newMinimalKnot("test-knot", "default") 53 + knot.Spec.Hostname = "knot.example.com" 54 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 55 + Route: &tangledv1alpha1.KnotRouteSpec{ 56 + Enabled: true, 57 + WildcardPolicy: "None", 58 + TLS: &tangledv1alpha1.KnotRouteTLSSpec{ 59 + Termination: "edge", 60 + InsecureEdgeTerminationPolicy: "Redirect", 61 + }, 62 + }, 63 + } 64 + 65 + routeSpec := knot.Spec.OpenShift.Route 66 + 67 + require.NotNil(t, routeSpec.TLS) 68 + assert.Equal(t, "edge", routeSpec.TLS.Termination) 69 + assert.Equal(t, "Redirect", routeSpec.TLS.InsecureEdgeTerminationPolicy) 70 + }) 71 + 72 + t.Run("builds route spec with custom TLS certificates", func(t *testing.T) { 73 + knot := newMinimalKnot("test-knot", "default") 74 + knot.Spec.Hostname = "knot.example.com" 75 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 76 + Route: &tangledv1alpha1.KnotRouteSpec{ 77 + Enabled: true, 78 + WildcardPolicy: "None", 79 + TLS: &tangledv1alpha1.KnotRouteTLSSpec{ 80 + Termination: "edge", 81 + Certificate: "test-certificate-data", 82 + Key: "test-key-data", 83 + CACertificate: "test-ca-certificate-data", 84 + DestinationCACertificate: "test-dest-ca-certificate-data", 85 + }, 86 + }, 87 + } 88 + 89 + routeSpec := knot.Spec.OpenShift.Route 90 + 91 + require.NotNil(t, routeSpec.TLS) 92 + assert.Equal(t, "edge", routeSpec.TLS.Termination) 93 + assert.Equal(t, "test-certificate-data", routeSpec.TLS.Certificate) 94 + assert.Equal(t, "test-key-data", routeSpec.TLS.Key) 95 + assert.Equal(t, "test-ca-certificate-data", routeSpec.TLS.CACertificate) 96 + assert.Equal(t, "test-dest-ca-certificate-data", routeSpec.TLS.DestinationCACertificate) 97 + }) 98 + 99 + t.Run("builds route spec with annotations", func(t *testing.T) { 100 + knot := newMinimalKnot("test-knot", "default") 101 + knot.Spec.Hostname = "knot.example.com" 102 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 103 + Route: &tangledv1alpha1.KnotRouteSpec{ 104 + Enabled: true, 105 + WildcardPolicy: "None", 106 + Annotations: map[string]string{ 107 + "haproxy.router.openshift.io/timeout": "60s", 108 + }, 109 + }, 110 + } 111 + 112 + routeSpec := knot.Spec.OpenShift.Route 113 + 114 + require.NotNil(t, routeSpec.Annotations) 115 + assert.Equal(t, "60s", routeSpec.Annotations["haproxy.router.openshift.io/timeout"]) 116 + }) 117 + } 118 + 119 + func TestBuildSCCSpec(t *testing.T) { 120 + t.Run("builds SCC spec with defaults", func(t *testing.T) { 121 + knot := newMinimalKnot("test-knot", "default") 122 + knot.Spec.ServiceAccountName = "test-sa" 123 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 124 + SCC: &tangledv1alpha1.KnotSCCSpec{ 125 + Create: true, 126 + }, 127 + } 128 + 129 + sccSpec := knot.Spec.OpenShift.SCC 130 + 131 + assert.True(t, sccSpec.Create) 132 + assert.False(t, sccSpec.AllowPrivilegedContainer) 133 + assert.False(t, sccSpec.AllowHostNetwork) 134 + assert.False(t, sccSpec.AllowHostPorts) 135 + assert.False(t, sccSpec.AllowHostPID) 136 + assert.False(t, sccSpec.AllowHostIPC) 137 + assert.False(t, sccSpec.ReadOnlyRootFilesystem) 138 + }) 139 + 140 + t.Run("builds SCC spec with custom values", func(t *testing.T) { 141 + knot := newMinimalKnot("test-knot", "default") 142 + knot.Spec.ServiceAccountName = "test-sa" 143 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 144 + SCC: &tangledv1alpha1.KnotSCCSpec{ 145 + Create: true, 146 + Name: "custom-scc", 147 + RunAsUser: "RunAsAny", 148 + SELinuxContext: "RunAsAny", 149 + FSGroup: "RunAsAny", 150 + SupplementalGroups: "RunAsAny", 151 + Volumes: []string{"configMap", "secret"}, 152 + }, 153 + } 154 + 155 + sccSpec := knot.Spec.OpenShift.SCC 156 + 157 + assert.Equal(t, "custom-scc", sccSpec.Name) 158 + assert.Equal(t, "RunAsAny", sccSpec.RunAsUser) 159 + assert.Equal(t, "RunAsAny", sccSpec.SELinuxContext) 160 + assert.Equal(t, "RunAsAny", sccSpec.FSGroup) 161 + assert.Equal(t, "RunAsAny", sccSpec.SupplementalGroups) 162 + assert.Equal(t, []string{"configMap", "secret"}, sccSpec.Volumes) 163 + }) 164 + 165 + t.Run("builds SCC spec with privileged settings", func(t *testing.T) { 166 + knot := newMinimalKnot("test-knot", "default") 167 + knot.Spec.ServiceAccountName = "test-sa" 168 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 169 + SCC: &tangledv1alpha1.KnotSCCSpec{ 170 + Create: true, 171 + AllowPrivilegedContainer: true, 172 + AllowHostNetwork: true, 173 + AllowHostPorts: true, 174 + AllowHostPID: true, 175 + AllowHostIPC: true, 176 + ReadOnlyRootFilesystem: true, 177 + }, 178 + } 179 + 180 + sccSpec := knot.Spec.OpenShift.SCC 181 + 182 + assert.True(t, sccSpec.AllowPrivilegedContainer) 183 + assert.True(t, sccSpec.AllowHostNetwork) 184 + assert.True(t, sccSpec.AllowHostPorts) 185 + assert.True(t, sccSpec.AllowHostPID) 186 + assert.True(t, sccSpec.AllowHostIPC) 187 + assert.True(t, sccSpec.ReadOnlyRootFilesystem) 188 + }) 189 + } 190 + 191 + func TestOpenShiftIntegration(t *testing.T) { 192 + ctx := context.Background() 193 + 194 + t.Run("reconcile skips OpenShift resources when not OpenShift", func(t *testing.T) { 195 + knot := newMinimalKnot("test-knot", "default") 196 + knot.Finalizers = []string{knotFinalizer} 197 + knot.Spec.Hostname = "knot.example.com" 198 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 199 + Route: &tangledv1alpha1.KnotRouteSpec{ 200 + Enabled: true, 201 + }, 202 + SCC: &tangledv1alpha1.KnotSCCSpec{ 203 + Create: true, 204 + }, 205 + } 206 + 207 + r := newTestReconciler(knot) 208 + r.IsOpenShift = false 209 + 210 + _, err := r.Reconcile(ctx, newReconcileRequest("test-knot", "default")) 211 + 212 + require.NoError(t, err) 213 + }) 214 + 215 + t.Run("IsOpenShift flag controls OpenShift resource creation", func(t *testing.T) { 216 + r := newTestReconciler() 217 + r.IsOpenShift = true 218 + assert.True(t, r.IsOpenShift) 219 + 220 + r.IsOpenShift = false 221 + assert.False(t, r.IsOpenShift) 222 + }) 223 + } 224 + 225 + func newReconcileRequest(name, namespace string) ctrl.Request { 226 + return ctrl.Request{ 227 + NamespacedName: types.NamespacedName{Name: name, Namespace: namespace}, 228 + } 229 + } 230 + 231 + func TestReconcileSecretValidation(t *testing.T) { 232 + ctx := context.Background() 233 + 234 + t.Run("regenerates secret when key is missing", func(t *testing.T) { 235 + existingSecret := &corev1.Secret{ 236 + ObjectMeta: metav1.ObjectMeta{ 237 + Name: "test-knot-secret", 238 + Namespace: "default", 239 + }, 240 + Data: map[string][]byte{ 241 + "WRONG_KEY": []byte("some-value"), 242 + }, 243 + } 244 + 245 + knot := newMinimalKnot("test-knot", "default") 246 + 247 + r := newTestReconciler(knot, existingSecret) 248 + err := r.reconcileSecret(ctx, knot) 249 + require.NoError(t, err) 250 + 251 + secret := &corev1.Secret{} 252 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 253 + require.NoError(t, err) 254 + 255 + var serverSecret string 256 + if val, ok := secret.Data["KNOT_SERVER_SECRET"]; ok { 257 + serverSecret = string(val) 258 + } else if val, ok := secret.StringData["KNOT_SERVER_SECRET"]; ok { 259 + serverSecret = val 260 + } 261 + assert.Len(t, serverSecret, 64) 262 + }) 263 + 264 + t.Run("regenerates secret when empty", func(t *testing.T) { 265 + existingSecret := &corev1.Secret{ 266 + ObjectMeta: metav1.ObjectMeta{ 267 + Name: "test-knot-secret", 268 + Namespace: "default", 269 + }, 270 + Data: map[string][]byte{}, 271 + } 272 + 273 + knot := newMinimalKnot("test-knot", "default") 274 + 275 + r := newTestReconciler(knot, existingSecret) 276 + err := r.reconcileSecret(ctx, knot) 277 + require.NoError(t, err) 278 + 279 + secret := &corev1.Secret{} 280 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 281 + require.NoError(t, err) 282 + 283 + var serverSecret string 284 + if val, ok := secret.Data["KNOT_SERVER_SECRET"]; ok { 285 + serverSecret = string(val) 286 + } else if val, ok := secret.StringData["KNOT_SERVER_SECRET"]; ok { 287 + serverSecret = val 288 + } 289 + assert.Len(t, serverSecret, 64) 290 + }) 291 + 292 + t.Run("preserves existing valid secret", func(t *testing.T) { 293 + existingSecret := &corev1.Secret{ 294 + ObjectMeta: metav1.ObjectMeta{ 295 + Name: "test-knot-secret", 296 + Namespace: "default", 297 + }, 298 + Data: map[string][]byte{ 299 + "KNOT_SERVER_SECRET": []byte("existing-secret-value-with-64-chars-padded-to-reach-the-length!!"), 300 + }, 301 + } 302 + 303 + knot := newMinimalKnot("test-knot", "default") 304 + 305 + r := newTestReconciler(knot, existingSecret) 306 + err := r.reconcileSecret(ctx, knot) 307 + require.NoError(t, err) 308 + 309 + secret := &corev1.Secret{} 310 + err = r.Get(ctx, types.NamespacedName{Name: "test-knot-secret", Namespace: "default"}, secret) 311 + require.NoError(t, err) 312 + 313 + assert.Equal(t, "existing-secret-value-with-64-chars-padded-to-reach-the-length!!", string(secret.Data["KNOT_SERVER_SECRET"])) 314 + }) 315 + } 316 + 317 + func TestOpenShiftDetectionLogic(t *testing.T) { 318 + t.Run("detection logic uses correct error handling", func(t *testing.T) { 319 + r := newTestReconciler() 320 + 321 + ctx := context.Background() 322 + result := r.DetectOpenShift(ctx) 323 + 324 + assert.True(t, result) 325 + }) 326 + } 327 + 328 + func TestOpenShiftSpecParsing(t *testing.T) { 329 + t.Run("parses OpenShift spec correctly", func(t *testing.T) { 330 + knot := newMinimalKnot("test-knot", "default") 331 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 332 + Route: &tangledv1alpha1.KnotRouteSpec{ 333 + Enabled: true, 334 + WildcardPolicy: "Subdomain", 335 + Annotations: map[string]string{ 336 + "key": "value", 337 + }, 338 + TLS: &tangledv1alpha1.KnotRouteTLSSpec{ 339 + Termination: "reencrypt", 340 + InsecureEdgeTerminationPolicy: "Allow", 341 + }, 342 + }, 343 + SCC: &tangledv1alpha1.KnotSCCSpec{ 344 + Create: true, 345 + Name: "my-scc", 346 + RunAsUser: "MustRunAsNonRoot", 347 + }, 348 + } 349 + 350 + require.NotNil(t, knot.Spec.OpenShift) 351 + require.NotNil(t, knot.Spec.OpenShift.Route) 352 + require.NotNil(t, knot.Spec.OpenShift.SCC) 353 + 354 + assert.True(t, knot.Spec.OpenShift.Route.Enabled) 355 + assert.Equal(t, "Subdomain", knot.Spec.OpenShift.Route.WildcardPolicy) 356 + assert.Equal(t, "value", knot.Spec.OpenShift.Route.Annotations["key"]) 357 + assert.Equal(t, "reencrypt", knot.Spec.OpenShift.Route.TLS.Termination) 358 + assert.Equal(t, "Allow", knot.Spec.OpenShift.Route.TLS.InsecureEdgeTerminationPolicy) 359 + 360 + assert.True(t, knot.Spec.OpenShift.SCC.Create) 361 + assert.Equal(t, "my-scc", knot.Spec.OpenShift.SCC.Name) 362 + assert.Equal(t, "MustRunAsNonRoot", knot.Spec.OpenShift.SCC.RunAsUser) 363 + }) 364 + 365 + t.Run("handles nil OpenShift spec", func(t *testing.T) { 366 + knot := newMinimalKnot("test-knot", "default") 367 + knot.Spec.OpenShift = nil 368 + 369 + assert.Nil(t, knot.Spec.OpenShift) 370 + }) 371 + 372 + t.Run("handles partial OpenShift spec", func(t *testing.T) { 373 + knot := newMinimalKnot("test-knot", "default") 374 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 375 + Route: &tangledv1alpha1.KnotRouteSpec{ 376 + Enabled: true, 377 + }, 378 + } 379 + 380 + require.NotNil(t, knot.Spec.OpenShift) 381 + require.NotNil(t, knot.Spec.OpenShift.Route) 382 + assert.Nil(t, knot.Spec.OpenShift.SCC) 383 + assert.True(t, knot.Spec.OpenShift.Route.Enabled) 384 + }) 385 + } 386 + 387 + func TestSCCNameGeneration(t *testing.T) { 388 + t.Run("uses custom name when provided", func(t *testing.T) { 389 + knot := newMinimalKnot("test-knot", "default") 390 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 391 + SCC: &tangledv1alpha1.KnotSCCSpec{ 392 + Create: true, 393 + Name: "custom-scc-name", 394 + }, 395 + } 396 + 397 + sccName := knot.Spec.OpenShift.SCC.Name 398 + assert.Equal(t, "custom-scc-name", sccName) 399 + }) 400 + 401 + t.Run("falls back to empty when name not provided", func(t *testing.T) { 402 + knot := newMinimalKnot("test-knot", "default") 403 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 404 + SCC: &tangledv1alpha1.KnotSCCSpec{ 405 + Create: true, 406 + }, 407 + } 408 + 409 + sccName := knot.Spec.OpenShift.SCC.Name 410 + assert.Equal(t, "", sccName) 411 + }) 412 + } 413 + 414 + func TestRouteTLSTerminationModes(t *testing.T) { 415 + testCases := []struct { 416 + name string 417 + termination string 418 + }{ 419 + {"edge termination", "edge"}, 420 + {"passthrough termination", "passthrough"}, 421 + {"reencrypt termination", "reencrypt"}, 422 + } 423 + 424 + for _, tc := range testCases { 425 + t.Run(tc.name, func(t *testing.T) { 426 + knot := newMinimalKnot("test-knot", "default") 427 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 428 + Route: &tangledv1alpha1.KnotRouteSpec{ 429 + Enabled: true, 430 + TLS: &tangledv1alpha1.KnotRouteTLSSpec{ 431 + Termination: tc.termination, 432 + }, 433 + }, 434 + } 435 + 436 + assert.Equal(t, tc.termination, knot.Spec.OpenShift.Route.TLS.Termination) 437 + }) 438 + } 439 + } 440 + 441 + func TestInsecureEdgeTerminationPolicies(t *testing.T) { 442 + testCases := []struct { 443 + name string 444 + policy string 445 + }{ 446 + {"Allow policy", "Allow"}, 447 + {"Redirect policy", "Redirect"}, 448 + {"None policy", "None"}, 449 + } 450 + 451 + for _, tc := range testCases { 452 + t.Run(tc.name, func(t *testing.T) { 453 + knot := newMinimalKnot("test-knot", "default") 454 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 455 + Route: &tangledv1alpha1.KnotRouteSpec{ 456 + Enabled: true, 457 + TLS: &tangledv1alpha1.KnotRouteTLSSpec{ 458 + Termination: "edge", 459 + InsecureEdgeTerminationPolicy: tc.policy, 460 + }, 461 + }, 462 + } 463 + 464 + assert.Equal(t, tc.policy, knot.Spec.OpenShift.Route.TLS.InsecureEdgeTerminationPolicy) 465 + }) 466 + } 467 + } 468 + 469 + func TestSCCVolumeTypes(t *testing.T) { 470 + t.Run("supports all standard volume types", func(t *testing.T) { 471 + volumes := []string{ 472 + "configMap", 473 + "downwardAPI", 474 + "emptyDir", 475 + "persistentVolumeClaim", 476 + "projected", 477 + "secret", 478 + } 479 + 480 + knot := newMinimalKnot("test-knot", "default") 481 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 482 + SCC: &tangledv1alpha1.KnotSCCSpec{ 483 + Create: true, 484 + Volumes: volumes, 485 + }, 486 + } 487 + 488 + assert.Equal(t, volumes, knot.Spec.OpenShift.SCC.Volumes) 489 + assert.Len(t, knot.Spec.OpenShift.SCC.Volumes, 6) 490 + }) 491 + 492 + t.Run("supports custom volume types", func(t *testing.T) { 493 + volumes := []string{"hostPath", "nfs"} 494 + 495 + knot := newMinimalKnot("test-knot", "default") 496 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 497 + SCC: &tangledv1alpha1.KnotSCCSpec{ 498 + Create: true, 499 + Volumes: volumes, 500 + }, 501 + } 502 + 503 + assert.Equal(t, volumes, knot.Spec.OpenShift.SCC.Volumes) 504 + }) 505 + } 506 + 507 + func TestWildcardPolicies(t *testing.T) { 508 + testCases := []struct { 509 + name string 510 + policy string 511 + }{ 512 + {"None policy", "None"}, 513 + {"Subdomain policy", "Subdomain"}, 514 + } 515 + 516 + for _, tc := range testCases { 517 + t.Run(tc.name, func(t *testing.T) { 518 + knot := newMinimalKnot("test-knot", "default") 519 + knot.Spec.OpenShift = &tangledv1alpha1.KnotOpenShiftSpec{ 520 + Route: &tangledv1alpha1.KnotRouteSpec{ 521 + Enabled: true, 522 + WildcardPolicy: tc.policy, 523 + }, 524 + } 525 + 526 + assert.Equal(t, tc.policy, knot.Spec.OpenShift.Route.WildcardPolicy) 527 + }) 528 + } 529 + }